diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index 01c57a6b9a..51fe0b035d 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; - [TestCase(4.2038001515546597d, "diffcalc-test")] + [TestCase(4.2058561036909863d, "diffcalc-test")] public void Test(double expected, string name) => base.Test(expected, name); diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 8cfda5d532..f3b88bd928 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Catch.Difficulty.Skills; +using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Difficulty; @@ -88,5 +89,13 @@ namespace osu.Game.Rulesets.Catch.Difficulty { new Movement(), }; + + protected override Mod[] DifficultyAdjustmentMods => new Mod[] + { + new CatchModDoubleTime(), + new CatchModHalfTime(), + new CatchModHardRock(), + new CatchModEasy(), + }; } } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 61bb4335f3..2adc156efd 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -1,7 +1,6 @@ // 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.Collections.Generic; using System.Linq; using osu.Game.Audio; @@ -25,6 +24,11 @@ namespace osu.Game.Rulesets.Catch.Objects public double Velocity; public double TickDistance; + /// + /// The length of one span of this . + /// + public double SpanDuration => Duration / this.SpanCount(); + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -41,19 +45,6 @@ namespace osu.Game.Rulesets.Catch.Objects protected override void CreateNestedHitObjects() { base.CreateNestedHitObjects(); - createTicks(); - } - - private void createTicks() - { - if (TickDistance == 0) - return; - - var length = Path.Distance; - var tickDistance = Math.Min(TickDistance, length); - var spanDuration = length / Velocity; - - var minDistanceFromEnd = Velocity * 0.01; var tickSamples = Samples.Select(s => new SampleInfo { @@ -62,81 +53,59 @@ namespace osu.Game.Rulesets.Catch.Objects Volume = s.Volume }).ToList(); - AddNested(new Fruit + SliderEventDescriptor? lastEvent = null; + + foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset)) { - Samples = Samples, - StartTime = StartTime, - X = X - }); - - double lastTickTime = StartTime; - - for (int span = 0; span < this.SpanCount(); span++) - { - var spanStartTime = StartTime + span * spanDuration; - var reversed = span % 2 == 1; - - for (double d = tickDistance;; d += tickDistance) + // generate tiny droplets since the last point + if (lastEvent != null) { - bool isLastTick = false; - if (d + minDistanceFromEnd >= length) + double sinceLastTick = e.Time - lastEvent.Value.Time; + + if (sinceLastTick > 80) { - d = length; - isLastTick = true; - } + double timeBetweenTiny = sinceLastTick; + while (timeBetweenTiny > 100) + timeBetweenTiny /= 2; - var timeProgress = d / length; - var distanceProgress = reversed ? 1 - timeProgress : timeProgress; - - double time = spanStartTime + timeProgress * spanDuration; - - if (LegacyLastTickOffset != null) - { - // If we're the last tick, apply the legacy offset - if (span == this.SpanCount() - 1 && isLastTick) - time = Math.Max(StartTime + Duration / 2, time - LegacyLastTickOffset.Value); - } - - int tinyTickCount = 1; - double tinyTickInterval = time - lastTickTime; - while (tinyTickInterval > 100 && tinyTickCount < 10000) - { - tinyTickInterval /= 2; - tinyTickCount *= 2; - } - - for (int tinyTickIndex = 0; tinyTickIndex < tinyTickCount - 1; tinyTickIndex++) - { - var t = lastTickTime + (tinyTickIndex + 1) * tinyTickInterval; - double progress = reversed ? 1 - (t - spanStartTime) / spanDuration : (t - spanStartTime) / spanDuration; - - AddNested(new TinyDroplet + for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny) { - StartTime = t, - X = X + Path.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH, - Samples = tickSamples - }); + AddNested(new TinyDroplet + { + Samples = tickSamples, + StartTime = t + lastEvent.Value.Time, + X = X + Path.PositionAt( + lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH, + }); + } } - - lastTickTime = time; - - if (isLastTick) - break; - - AddNested(new Droplet - { - StartTime = time, - X = X + Path.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH, - Samples = tickSamples - }); } - AddNested(new Fruit + // this also includes LegacyLastTick and this is used for TinyDroplet generation above. + // this means that the final segment of TinyDroplets are increasingly mistimed where LegacyLastTickOffset is being applied. + lastEvent = e; + + switch (e.Type) { - Samples = Samples, - StartTime = spanStartTime + spanDuration, - X = X + Path.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH - }); + case SliderEventType.Tick: + AddNested(new Droplet + { + Samples = tickSamples, + StartTime = e.Time, + X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, + }); + break; + case SliderEventType.Head: + case SliderEventType.Tail: + case SliderEventType.Repeat: + AddNested(new Fruit + { + Samples = Samples, + StartTime = e.Time, + X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, + }); + break; + } } } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 5874bac7f6..8797f014df 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -22,22 +22,15 @@ namespace osu.Game.Rulesets.Mania.UI JudgementText.Font = JudgementText.Font.With(size: 25); } - protected override void LoadComplete() + protected override double FadeInDuration => 50; + + protected override void ApplyHitAnimations() { - base.LoadComplete(); + JudgementBody.ScaleTo(0.8f); + JudgementBody.ScaleTo(1, 250, Easing.OutElastic); - this.FadeInFromZero(50, Easing.OutQuint); - - if (Result.IsHit) - { - JudgementBody.ScaleTo(0.8f); - JudgementBody.ScaleTo(1, 250, Easing.OutElastic); - - JudgementBody.Delay(50).ScaleTo(0.75f, 250); - this.Delay(50).FadeOut(200); - } - - Expire(); + JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250); + this.Delay(FadeInDuration).FadeOut(200); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseOsuPlayer.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseOsuPlayer.cs new file mode 100644 index 0000000000..9f13c19390 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseOsuPlayer.cs @@ -0,0 +1,16 @@ +// 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; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestCaseOsuPlayer : Game.Tests.Visual.TestCasePlayer + { + public TestCaseOsuPlayer() + : base(new OsuRuleset()) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 2512e74da7..938a2293ba 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osuTK; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -16,12 +15,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } - protected override void LoadComplete() + protected override void ApplyHitAnimations() { - if (Result.Type != HitResult.Miss) - JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); - - base.LoadComplete(); + JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); + base.ApplyHitAnimations(); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 1b2e2c1f47..e41c568403 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -132,6 +132,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces base.ClearTransformsAfter(time, false, targetMember); } + public override void ApplyTransformsAt(double time, bool propagateChildren = false) + { + // For the same reasons as above w.r.t rewinding, we shouldn't propagate to children here either. + base.ApplyTransformsAt(time, false); + } + private bool tracking; public bool Tracking diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 345f599b9d..1afbacc01e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -1,7 +1,6 @@ // 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 osuTK; using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; @@ -155,116 +154,76 @@ namespace osu.Game.Rulesets.Osu.Objects { base.CreateNestedHitObjects(); - createSliderEnds(); - createTicks(); - createRepeatPoints(); - - if (LegacyLastTickOffset != null) - TailCircle.StartTime = Math.Max(StartTime + Duration / 2, TailCircle.StartTime - LegacyLastTickOffset.Value); - } - - private void createSliderEnds() - { - HeadCircle = new SliderCircle + foreach (var e in + SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset)) { - StartTime = StartTime, - Position = Position, - Samples = getNodeSamples(0), - SampleControlPoint = SampleControlPoint, - IndexInCurrentCombo = IndexInCurrentCombo, - ComboIndex = ComboIndex, - }; + var firstSample = Samples.Find(s => s.Name == SampleInfo.HIT_NORMAL) + ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933) + var sampleList = new List(); - TailCircle = new SliderTailCircle(this) - { - StartTime = EndTime, - Position = EndPosition, - IndexInCurrentCombo = IndexInCurrentCombo, - ComboIndex = ComboIndex, - }; - - AddNested(HeadCircle); - AddNested(TailCircle); - } - - private void createTicks() - { - // A very lenient maximum length of a slider for ticks to be generated. - // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. - const double max_length = 100000; - - var length = Math.Min(max_length, Path.Distance); - var tickDistance = MathHelper.Clamp(TickDistance, 0, length); - - if (tickDistance == 0) return; - - var minDistanceFromEnd = Velocity * 10; - - var spanCount = this.SpanCount(); - - for (var span = 0; span < spanCount; span++) - { - var spanStartTime = StartTime + span * SpanDuration; - var reversed = span % 2 == 1; - - for (var d = tickDistance; d <= length; d += tickDistance) - { - if (d > length - minDistanceFromEnd) - break; - - var distanceProgress = d / length; - var timeProgress = reversed ? 1 - distanceProgress : distanceProgress; - - var firstSample = Samples.Find(s => s.Name == SampleInfo.HIT_NORMAL) - ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933) - var sampleList = new List(); - - if (firstSample != null) - sampleList.Add(new SampleInfo - { - Bank = firstSample.Bank, - Volume = firstSample.Volume, - Name = @"slidertick", - }); - - AddNested(new SliderTick + if (firstSample != null) + sampleList.Add(new SampleInfo { - SpanIndex = span, - SpanStartTime = spanStartTime, - StartTime = spanStartTime + timeProgress * SpanDuration, - Position = Position + Path.PositionAt(distanceProgress), - StackHeight = StackHeight, - Scale = Scale, - Samples = sampleList + Bank = firstSample.Bank, + Volume = firstSample.Volume, + Name = @"slidertick", }); + + switch (e.Type) + { + case SliderEventType.Tick: + AddNested(new SliderTick + { + SpanIndex = e.SpanIndex, + SpanStartTime = e.SpanStartTime, + StartTime = e.Time, + Position = Position + Path.PositionAt(e.PathProgress), + StackHeight = StackHeight, + Scale = Scale, + Samples = sampleList + }); + break; + case SliderEventType.Head: + AddNested(HeadCircle = new SliderCircle + { + StartTime = e.Time, + Position = Position, + Samples = getNodeSamples(0), + SampleControlPoint = SampleControlPoint, + IndexInCurrentCombo = IndexInCurrentCombo, + ComboIndex = ComboIndex, + }); + break; + case SliderEventType.LegacyLastTick: + // we need to use the LegacyLastTick here for compatibility reasons (difficulty). + // it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay. + // if this is to change, we should revisit this. + AddNested(TailCircle = new SliderTailCircle(this) + { + StartTime = e.Time, + Position = EndPosition, + IndexInCurrentCombo = IndexInCurrentCombo, + ComboIndex = ComboIndex, + }); + break; + case SliderEventType.Repeat: + AddNested(new RepeatPoint + { + RepeatIndex = e.SpanIndex, + SpanDuration = SpanDuration, + StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, + Position = Position + Path.PositionAt(e.PathProgress), + StackHeight = StackHeight, + Scale = Scale, + Samples = getNodeSamples(e.SpanIndex + 1) + }); + break; } } } - private void createRepeatPoints() - { - for (int repeatIndex = 0, repeat = 1; repeatIndex < RepeatCount; repeatIndex++, repeat++) - { - AddNested(new RepeatPoint - { - RepeatIndex = repeatIndex, - SpanDuration = SpanDuration, - StartTime = StartTime + repeat * SpanDuration, - Position = Position + Path.PositionAt(repeat % 2), - StackHeight = StackHeight, - Scale = Scale, - Samples = getNodeSamples(1 + repeatIndex) - }); - } - } - - private List getNodeSamples(int nodeIndex) - { - if (nodeIndex < NodeSamples.Count) - return NodeSamples[nodeIndex]; - - return Samples; - } + private List getNodeSamples(int nodeIndex) => + nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples; public override Judgement CreateJudgement() => new OsuJudgement(); } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 43a2ae0fbb..4f2af64161 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -8,6 +8,10 @@ using osu.Game.Rulesets.Osu.Judgements; namespace osu.Game.Rulesets.Osu.Objects { + /// + /// Note that this should not be used for timing correctness. + /// See usage in for more information. + /// public class SliderTailCircle : SliderCircle { private readonly IBindable pathBindable = new Bindable(); diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index 90841f11f5..943adaed4b 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs @@ -39,12 +39,10 @@ namespace osu.Game.Rulesets.Taiko.UI } } - protected override void LoadComplete() + protected override void ApplyHitAnimations() { - if (Result.IsHit) - this.MoveToY(-100, 500); - - base.LoadComplete(); + this.MoveToY(-100, 500); + base.ApplyHitAnimations(); } } } diff --git a/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs b/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs new file mode 100644 index 0000000000..b3863bcf44 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs @@ -0,0 +1,72 @@ +// 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.Globalization; +using NUnit.Framework; +using osu.Game.Beatmaps.Formats; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + [TestFixture] + public class ParsingTest + { + [Test] + public void TestNaNHandling() => allThrow("NaN"); + + [Test] + public void TestBadStringHandling() => allThrow("Random string 123"); + + [TestCase(Parsing.MAX_PARSE_VALUE)] + [TestCase(-1)] + [TestCase(0)] + [TestCase(1)] + [TestCase(-Parsing.MAX_PARSE_VALUE)] + [TestCase(10, 10)] + [TestCase(-10, 10)] + public void TestValidRanges(double input, double limit = Parsing.MAX_PARSE_VALUE) + { + Assert.AreEqual(Parsing.ParseInt((input).ToString(CultureInfo.InvariantCulture), (int)limit), (int)input); + Assert.AreEqual(Parsing.ParseFloat((input).ToString(CultureInfo.InvariantCulture), (float)limit), (float)input); + Assert.AreEqual(Parsing.ParseDouble((input).ToString(CultureInfo.InvariantCulture), limit), input); + } + + [TestCase(double.PositiveInfinity)] + [TestCase(double.NegativeInfinity)] + [TestCase(999999999999)] + [TestCase(Parsing.MAX_PARSE_VALUE * 1.1)] + [TestCase(-Parsing.MAX_PARSE_VALUE * 1.1)] + [TestCase(11, 10)] + [TestCase(-11, 10)] + public void TestOutOfRangeHandling(double input, double limit = Parsing.MAX_PARSE_VALUE) + => allThrow(input.ToString(CultureInfo.InvariantCulture), limit); + + private void allThrow(string input, double limit = Parsing.MAX_PARSE_VALUE) + where T : Exception + { + Assert.Throws(getIntParseException(input) ?? typeof(T), () => Parsing.ParseInt(input, (int)limit)); + Assert.Throws(() => Parsing.ParseFloat(input, (float)limit)); + Assert.Throws(() => Parsing.ParseDouble(input, limit)); + } + + /// + /// may not be able to parse some inputs. + /// In this case we expect to receive the raw parsing exception. + /// + /// The input attempting to be parsed. + /// The type of exception thrown by . Null if no exception is thrown. + private Type getIntParseException(string input) + { + try + { + var _ = int.Parse(input); + } + catch (Exception e) + { + return e.GetType(); + } + + return null; + } + } +} diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 5b8bdd8a51..0f65f7f82e 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -207,6 +207,41 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [TestCase(true)] + [TestCase(false)] + public void TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"TestImportThenDeleteThenImport-{set}")) + { + try + { + var osu = loadOsu(host); + + var imported = LoadOszIntoOsu(osu); + + if (set) + imported.OnlineBeatmapSetID = 1234; + else + imported.Beatmaps.First().OnlineBeatmapID = 1234; + + osu.Dependencies.Get().Update(imported); + + deleteBeatmapSet(imported, osu); + + var importedSecondTime = LoadOszIntoOsu(osu); + + // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched) + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + } + finally + { + host.Exit(); + } + } + } + [Test] [NonParallelizable] [Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")] diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapDetailArea.cs b/osu.Game.Tests/Visual/TestCaseBeatmapDetailArea.cs index c23075a127..6cc3982f9c 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapDetailArea.cs @@ -1,8 +1,12 @@ // 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.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Screens.Select; using osuTK; @@ -12,14 +16,145 @@ namespace osu.Game.Tests.Visual [System.ComponentModel.Description("PlaySongSelect leaderboard/details area")] public class TestCaseBeatmapDetailArea : OsuTestCase { + public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapDetails) }; + public TestCaseBeatmapDetailArea() { - Add(new BeatmapDetailArea + BeatmapDetailArea detailsArea; + Add(detailsArea = new BeatmapDetailArea { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(550f, 450f), }); + + AddStep("all metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap + { + BeatmapInfo = + { + Version = "All Metrics", + Metadata = new BeatmapMetadata + { + Source = "osu!lazer", + Tags = "this beatmap has all the metrics", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 7, + DrainRate = 1, + OverallDifficulty = 5.7f, + ApproachRate = 3.5f, + }, + StarDifficulty = 5.3f, + Metrics = new BeatmapMetrics + { + Ratings = Enumerable.Range(0, 11), + Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), + Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6), + }, + } + } + ); + + AddStep("all except source", () => detailsArea.Beatmap = new DummyWorkingBeatmap + { + BeatmapInfo = + { + Version = "All Metrics", + Metadata = new BeatmapMetadata + { + Tags = "this beatmap has all the metrics", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 7, + DrainRate = 1, + OverallDifficulty = 5.7f, + ApproachRate = 3.5f, + }, + StarDifficulty = 5.3f, + Metrics = new BeatmapMetrics + { + Ratings = Enumerable.Range(0, 11), + Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), + Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6), + }, + } + }); + + AddStep("ratings", () => detailsArea.Beatmap = new DummyWorkingBeatmap + { + BeatmapInfo = + { + Version = "Only Ratings", + Metadata = new BeatmapMetadata + { + Source = "osu!lazer", + Tags = "this beatmap has ratings metrics but not retries or fails", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 6, + DrainRate = 9, + OverallDifficulty = 6, + ApproachRate = 6, + }, + StarDifficulty = 4.8f, + Metrics = new BeatmapMetrics + { + Ratings = Enumerable.Range(0, 11), + }, + } + }); + + AddStep("fails+retries", () => detailsArea.Beatmap = new DummyWorkingBeatmap + { + BeatmapInfo = + { + Version = "Only Retries and Fails", + Metadata = new BeatmapMetadata + { + Source = "osu!lazer", + Tags = "this beatmap has retries and fails but no ratings", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 3.7f, + DrainRate = 6, + OverallDifficulty = 6, + ApproachRate = 7, + }, + StarDifficulty = 2.91f, + Metrics = new BeatmapMetrics + { + Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), + Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6), + }, + } + }); + + AddStep("null metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap + { + BeatmapInfo = + { + Version = "No Metrics", + Metadata = new BeatmapMetadata + { + Source = "osu!lazer", + Tags = "this beatmap has no metrics", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 5, + DrainRate = 5, + OverallDifficulty = 5.5f, + ApproachRate = 6.5f, + }, + StarDifficulty = 1.97f, + } + }); + + AddStep("null beatmap", () => detailsArea.Beatmap = null); } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 88f5e777e3..711aa0b79b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -102,11 +102,14 @@ namespace osu.Game.Beatmaps b.BeatmapSet = beatmapSet; } - validateOnlineIds(beatmapSet.Beatmaps); + validateOnlineIds(beatmapSet); foreach (BeatmapInfo b in beatmapSet.Beatmaps) fetchAndPopulateOnlineValues(b, beatmapSet.Beatmaps); + } + protected override void PreImport(BeatmapSetInfo beatmapSet) + { // check if a set already exists with the same online id, delete if it does. if (beatmapSet.OnlineBeatmapSetID != null) { @@ -120,14 +123,30 @@ namespace osu.Game.Beatmaps } } - private void validateOnlineIds(List beatmaps) + private void validateOnlineIds(BeatmapSetInfo beatmapSet) { - var beatmapIds = beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); + var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); - // ensure all IDs are unique in this set and none match existing IDs in the local beatmap store. - if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1) || QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).Any()) - // remove all online IDs if any problems were found. - beatmaps.ForEach(b => b.OnlineBeatmapID = null); + // ensure all IDs are unique + if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) + { + resetIds(); + return; + } + + // find any existing beatmaps in the database that have matching online ids + var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList(); + + if (existingBeatmaps.Count > 0) + { + // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set. + // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted. + var existing = CheckForExisting(beatmapSet); + if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b))) + resetIds(); + } + + void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); } /// @@ -254,6 +273,18 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); + protected override bool CanUndelete(BeatmapSetInfo existing, BeatmapSetInfo import) + { + if (!base.CanUndelete(existing, import)) + return false; + + var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); + var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); + + // force re-import if we are not in a sane state. + return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds); + } + /// /// Returns a list of all usable s. /// diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index f0af09459f..ec75c1a1fb 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -45,24 +45,7 @@ namespace osu.Game.Beatmaps.Drawables protected override Drawable CreateDrawable(BeatmapInfo model) { - Drawable drawable; - - var localBeatmap = beatmaps.GetWorkingBeatmap(model); - - if (model?.BeatmapSet?.OnlineInfo != null) - { - drawable = new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType); - } - else if (localBeatmap.BeatmapInfo.ID != 0) - { - // Fall back to local background if one exists - drawable = new BeatmapBackgroundSprite(localBeatmap); - } - else - { - // Use the default background if somehow an online set does not exist and we don't have a local copy. - drawable = new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap); - } + Drawable drawable = getDrawableForModel(model); drawable.RelativeSizeAxes = Axes.Both; drawable.Anchor = Anchor.Centre; @@ -72,5 +55,16 @@ namespace osu.Game.Beatmaps.Drawables return drawable; } + + private Drawable getDrawableForModel(BeatmapInfo model) + { + // prefer online cover where available. + if (model?.BeatmapSet?.OnlineInfo != null) + return new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType); + + return model?.ID > 0 + ? new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(model)) + : new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap); + } } } diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 0aa1697bf8..f9df025be8 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -26,7 +26,12 @@ namespace osu.Game.Beatmaps Title = "no beatmaps available!" }, BeatmapSet = new BeatmapSetInfo(), - BaseDifficulty = new BeatmapDifficulty(), + BaseDifficulty = new BeatmapDifficulty + { + DrainRate = 0, + CircleSize = 0, + OverallDifficulty = 0, + }, Ruleset = new DummyRulesetInfo() }) { diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 4f19fbd323..a27126ad9c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -2,10 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Globalization; using System.IO; using System.Linq; using osu.Framework.IO.File; +using osu.Framework.Logging; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Beatmaps.ControlPoints; @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Formats public static void Register() { - AddDecoder(@"osu file format v", m => new LegacyBeatmapDecoder(int.Parse(m.Split('v').Last()))); + AddDecoder(@"osu file format v", m => new LegacyBeatmapDecoder(Parsing.ParseInt(m.Split('v').Last()))); } /// @@ -104,25 +104,25 @@ namespace osu.Game.Beatmaps.Formats metadata.AudioFile = FileSafety.PathStandardise(pair.Value); break; case @"AudioLeadIn": - beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value); + beatmap.BeatmapInfo.AudioLeadIn = Parsing.ParseInt(pair.Value); break; case @"PreviewTime": - metadata.PreviewTime = getOffsetTime(int.Parse(pair.Value)); + metadata.PreviewTime = getOffsetTime(Parsing.ParseInt(pair.Value)); break; case @"Countdown": - beatmap.BeatmapInfo.Countdown = int.Parse(pair.Value) == 1; + beatmap.BeatmapInfo.Countdown = Parsing.ParseInt(pair.Value) == 1; break; case @"SampleSet": defaultSampleBank = (LegacySampleBank)Enum.Parse(typeof(LegacySampleBank), pair.Value); break; case @"SampleVolume": - defaultSampleVolume = int.Parse(pair.Value); + defaultSampleVolume = Parsing.ParseInt(pair.Value); break; case @"StackLeniency": - beatmap.BeatmapInfo.StackLeniency = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + beatmap.BeatmapInfo.StackLeniency = Parsing.ParseFloat(pair.Value); break; case @"Mode": - beatmap.BeatmapInfo.RulesetID = int.Parse(pair.Value); + beatmap.BeatmapInfo.RulesetID = Parsing.ParseInt(pair.Value); switch (beatmap.BeatmapInfo.RulesetID) { @@ -142,13 +142,13 @@ namespace osu.Game.Beatmaps.Formats break; case @"LetterboxInBreaks": - beatmap.BeatmapInfo.LetterboxInBreaks = int.Parse(pair.Value) == 1; + beatmap.BeatmapInfo.LetterboxInBreaks = Parsing.ParseInt(pair.Value) == 1; break; case @"SpecialStyle": - beatmap.BeatmapInfo.SpecialStyle = int.Parse(pair.Value) == 1; + beatmap.BeatmapInfo.SpecialStyle = Parsing.ParseInt(pair.Value) == 1; break; case @"WidescreenStoryboard": - beatmap.BeatmapInfo.WidescreenStoryboard = int.Parse(pair.Value) == 1; + beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1; break; } } @@ -163,16 +163,16 @@ namespace osu.Game.Beatmaps.Formats beatmap.BeatmapInfo.StoredBookmarks = pair.Value; break; case @"DistanceSpacing": - beatmap.BeatmapInfo.DistanceSpacing = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + beatmap.BeatmapInfo.DistanceSpacing = Math.Max(0, Parsing.ParseDouble(pair.Value)); break; case @"BeatDivisor": - beatmap.BeatmapInfo.BeatDivisor = int.Parse(pair.Value); + beatmap.BeatmapInfo.BeatDivisor = Parsing.ParseInt(pair.Value); break; case @"GridSize": - beatmap.BeatmapInfo.GridSize = int.Parse(pair.Value); + beatmap.BeatmapInfo.GridSize = Parsing.ParseInt(pair.Value); break; case @"TimelineZoom": - beatmap.BeatmapInfo.TimelineZoom = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + beatmap.BeatmapInfo.TimelineZoom = Math.Max(0, Parsing.ParseDouble(pair.Value)); break; } } @@ -209,10 +209,10 @@ namespace osu.Game.Beatmaps.Formats beatmap.BeatmapInfo.Metadata.Tags = pair.Value; break; case @"BeatmapID": - beatmap.BeatmapInfo.OnlineBeatmapID = int.Parse(pair.Value); + beatmap.BeatmapInfo.OnlineBeatmapID = Parsing.ParseInt(pair.Value); break; case @"BeatmapSetID": - beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = int.Parse(pair.Value) }; + beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = Parsing.ParseInt(pair.Value) }; break; } } @@ -225,22 +225,22 @@ namespace osu.Game.Beatmaps.Formats switch (pair.Key) { case @"HPDrainRate": - difficulty.DrainRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + difficulty.DrainRate = Parsing.ParseFloat(pair.Value); break; case @"CircleSize": - difficulty.CircleSize = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + difficulty.CircleSize = Parsing.ParseFloat(pair.Value); break; case @"OverallDifficulty": - difficulty.OverallDifficulty = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + difficulty.OverallDifficulty = Parsing.ParseFloat(pair.Value); break; case @"ApproachRate": - difficulty.ApproachRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + difficulty.ApproachRate = Parsing.ParseFloat(pair.Value); break; case @"SliderMultiplier": - difficulty.SliderMultiplier = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + difficulty.SliderMultiplier = Parsing.ParseDouble(pair.Value); break; case @"SliderTickRate": - difficulty.SliderTickRate = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + difficulty.SliderTickRate = Parsing.ParseDouble(pair.Value); break; } } @@ -260,10 +260,12 @@ namespace osu.Game.Beatmaps.Formats beatmap.BeatmapInfo.Metadata.BackgroundFile = FileSafety.PathStandardise(filename); break; case EventType.Break: + double start = getOffsetTime(Parsing.ParseDouble(split[1])); + var breakEvent = new BreakPeriod { - StartTime = getOffsetTime(double.Parse(split[1], NumberFormatInfo.InvariantInfo)), - EndTime = getOffsetTime(double.Parse(split[2], NumberFormatInfo.InvariantInfo)) + StartTime = start, + EndTime = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2]))) }; if (!breakEvent.HasEffect) @@ -280,25 +282,25 @@ namespace osu.Game.Beatmaps.Formats { string[] split = line.Split(','); - double time = getOffsetTime(double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo)); - double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo); + double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim())); + double beatLength = Parsing.ParseDouble(split[1].Trim()); double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; if (split.Length >= 3) - timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)int.Parse(split[2]); + timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)Parsing.ParseInt(split[2]); LegacySampleBank sampleSet = defaultSampleBank; if (split.Length >= 4) - sampleSet = (LegacySampleBank)int.Parse(split[3]); + sampleSet = (LegacySampleBank)Parsing.ParseInt(split[3]); int customSampleBank = 0; if (split.Length >= 5) - customSampleBank = int.Parse(split[4]); + customSampleBank = Parsing.ParseInt(split[4]); int sampleVolume = defaultSampleVolume; if (split.Length >= 6) - sampleVolume = int.Parse(split[5]); + sampleVolume = Parsing.ParseInt(split[5]); bool timingChange = true; if (split.Length >= 7) @@ -308,7 +310,7 @@ namespace osu.Game.Beatmaps.Formats bool omitFirstBarSignature = false; if (split.Length >= 8) { - EffectFlags effectFlags = (EffectFlags)int.Parse(split[7]); + EffectFlags effectFlags = (EffectFlags)Parsing.ParseInt(split[7]); kiaiMode = effectFlags.HasFlag(EffectFlags.Kiai); omitFirstBarSignature = effectFlags.HasFlag(EffectFlags.OmitFirstBarLine); } @@ -348,8 +350,13 @@ namespace osu.Game.Beatmaps.Formats CustomSampleBank = customSampleBank }); } - catch (FormatException e) + catch (FormatException) { + Logger.Log("A timing point could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important); + } + catch (OverflowException) + { + Logger.Log("A timing point could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important); } } diff --git a/osu.Game/Beatmaps/Formats/Parsing.cs b/osu.Game/Beatmaps/Formats/Parsing.cs new file mode 100644 index 0000000000..c3efb8c760 --- /dev/null +++ b/osu.Game/Beatmaps/Formats/Parsing.cs @@ -0,0 +1,52 @@ +// 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.Globalization; + +namespace osu.Game.Beatmaps.Formats +{ + /// + /// Helper methods to parse from string to number and perform very basic validation. + /// + public static class Parsing + { + public const int MAX_COORDINATE_VALUE = 65536; + + public const double MAX_PARSE_VALUE = int.MaxValue; + + public static float ParseFloat(string input, float parseLimit = (float)MAX_PARSE_VALUE) + { + var output = float.Parse(input, CultureInfo.InvariantCulture); + + if (output < -parseLimit) throw new OverflowException("Value is too low"); + if (output > parseLimit) throw new OverflowException("Value is too high"); + + if (float.IsNaN(output)) throw new FormatException("Not a number"); + + return output; + } + + public static double ParseDouble(string input, double parseLimit = MAX_PARSE_VALUE) + { + var output = double.Parse(input, CultureInfo.InvariantCulture); + + if (output < -parseLimit) throw new OverflowException("Value is too low"); + if (output > parseLimit) throw new OverflowException("Value is too high"); + + if (double.IsNaN(output)) throw new FormatException("Not a number"); + + return output; + } + + public static int ParseInt(string input, int parseLimit = (int)MAX_PARSE_VALUE) + { + var output = int.Parse(input, CultureInfo.InvariantCulture); + + if (output < -parseLimit) throw new OverflowException("Value is too low"); + if (output > parseLimit) throw new OverflowException("Value is too high"); + + return output; + } + } +} diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 9ec184abd7..3805921ac2 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -300,21 +300,31 @@ namespace osu.Game.Database { if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}"); - var existing = CheckForExisting(item); - - if (existing != null) - { - Undelete(existing); - Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database); - handleEvent(() => ItemAdded?.Invoke(existing, true)); - return existing; - } - if (archive != null) item.Files = createFileInfos(archive, Files); Populate(item, archive); + var existing = CheckForExisting(item); + + if (existing != null) + { + if (CanUndelete(existing, item)) + { + Undelete(existing); + Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database); + handleEvent(() => ItemAdded?.Invoke(existing, true)); + return existing; + } + else + { + Delete(existing); + ModelStore.PurgeDeletable(s => s.ID == existing.ID); + } + } + + PreImport(item); + // import to store ModelStore.Add(item); } @@ -542,12 +552,29 @@ namespace osu.Game.Database { } + /// + /// Perform any final actions before the import to database executes. + /// + /// The model prepared for import. + protected virtual void PreImport(TModel model) + { + } + /// /// Check whether an existing model already exists for a new import item. /// - /// The new model proposed for import. Note that has not yet been run on this model. + /// The new model proposed for import. /// An existing model which matches the criteria to skip importing, else null. - protected virtual TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); + protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); + + /// + /// After an existing is found during an import process, the default behaviour is to restore the existing + /// item and skip the import. This method allows changing that behaviour. + /// + /// The existing model. + /// The newly imported model. + /// Whether the existing model should be restored and used. Returning false will delete the existing a force a re-import. + protected virtual bool CanUndelete(TModel existing, TModel import) => true; private DbSet queryModel() => ContextFactory.Get().Set(); diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs index c38cd19b42..026442f328 100644 --- a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs +++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs @@ -13,7 +13,6 @@ namespace osu.Game.Migrations protected override void Down(MigrationBuilder migrationBuilder) { - } } } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 5428279325..77f88ab4e7 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -320,6 +320,8 @@ namespace osu.Game.Overlays this.MoveToY(Height, transition_length, Easing.InSine); this.FadeOut(transition_length, Easing.InSine); + channelSelectionOverlay.State = Visibility.Hidden; + textbox.HoldFocus = false; base.PopOut(); } diff --git a/osu.Game/Overlays/Direct/DirectGridPanel.cs b/osu.Game/Overlays/Direct/DirectGridPanel.cs index 1413f0f885..b35dbde639 100644 --- a/osu.Game/Overlays/Direct/DirectGridPanel.cs +++ b/osu.Game/Overlays/Direct/DirectGridPanel.cs @@ -185,10 +185,7 @@ namespace osu.Game.Overlays.Direct Margin = new MarginPadding { Top = vertical_padding, Right = vertical_padding }, Children = new[] { - new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0) - { - Margin = new MarginPadding { Right = 1 }, - }, + new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0), new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0), }, }, diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs index 01393ad98b..d857a0f042 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -160,10 +160,7 @@ namespace osu.Game.Overlays.Direct Direction = FillDirection.Vertical, Children = new Drawable[] { - new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0) - { - Margin = new MarginPadding { Right = 1 }, - }, + new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0), new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0), new FillFlowContainer { diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 0dc74b6a88..a3d25a7a1c 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -134,9 +134,9 @@ namespace osu.Game.Overlays Filter.Tabs.Current.Value = DirectSortCriteria.Ranked; } }; - ((FilterControl)Filter).Ruleset.ValueChanged += _ => Scheduler.AddOnce(updateSearch); + ((FilterControl)Filter).Ruleset.ValueChanged += _ => queueUpdateSearch(); Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue); - Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => Scheduler.AddOnce(updateSearch); + Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => queueUpdateSearch(); Header.Tabs.Current.ValueChanged += tab => { @@ -144,24 +144,11 @@ namespace osu.Game.Overlays { currentQuery.Value = string.Empty; Filter.Tabs.Current.Value = (DirectSortCriteria)Header.Tabs.Current.Value; - Scheduler.AddOnce(updateSearch); + queueUpdateSearch(); } }; - currentQuery.ValueChanged += text => - { - queryChangedDebounce?.Cancel(); - - if (string.IsNullOrEmpty(text.NewValue)) - Scheduler.AddOnce(updateSearch); - else - { - BeatmapSets = null; - ResultAmounts = null; - - queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500); - } - }; + currentQuery.ValueChanged += text => queueUpdateSearch(!string.IsNullOrEmpty(text.NewValue)); currentQuery.BindTo(Filter.Search.Current); @@ -170,7 +157,7 @@ namespace osu.Game.Overlays if (Header.Tabs.Current.Value != DirectTab.Search && tab.NewValue != (DirectSortCriteria)Header.Tabs.Current.Value) Header.Tabs.Current.Value = DirectTab.Search; - Scheduler.AddOnce(updateSearch); + queueUpdateSearch(); }; updateResultCounts(); @@ -242,37 +229,42 @@ namespace osu.Game.Overlays // Queries are allowed to be run only on the first pop-in if (getSetsRequest == null) - Scheduler.AddOnce(updateSearch); + queueUpdateSearch(); } private SearchBeatmapSetsRequest getSetsRequest; - private readonly Bindable currentQuery = new Bindable(); + private readonly Bindable currentQuery = new Bindable(string.Empty); private ScheduledDelegate queryChangedDebounce; private PreviewTrackManager previewTrackManager; + private void queueUpdateSearch(bool queryTextChanged = false) + { + BeatmapSets = null; + ResultAmounts = null; + + getSetsRequest?.Cancel(); + + queryChangedDebounce?.Cancel(); + queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); + } + private void updateSearch() { - queryChangedDebounce?.Cancel(); - if (!IsLoaded) return; if (State == Visibility.Hidden) return; - BeatmapSets = null; - ResultAmounts = null; - - getSetsRequest?.Cancel(); - if (api == null) return; previewTrackManager.StopAnyPlaying(this); - getSetsRequest = new SearchBeatmapSetsRequest(currentQuery.Value ?? string.Empty, + getSetsRequest = new SearchBeatmapSetsRequest( + currentQuery.Value, ((FilterControl)Filter).Ruleset.Value, Filter.DisplayStyleControl.Dropdown.Current.Value, Filter.Tabs.Current.Value); //todo: sort direction (?) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 0d6e11c649..89db954c36 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -32,6 +32,12 @@ namespace osu.Game.Rulesets.Judgements protected Container JudgementBody; protected SpriteText JudgementText; + /// + /// Duration of initial fade in. + /// Default fade out will start immediately after this duration. + /// + protected virtual double FadeInDuration => 100; + /// /// Creates a drawable which visualises a . /// @@ -65,11 +71,19 @@ namespace osu.Game.Rulesets.Judgements }; } + protected virtual void ApplyHitAnimations() + { + JudgementBody.ScaleTo(0.9f); + JudgementBody.ScaleTo(1, 500, Easing.OutElastic); + + this.Delay(FadeInDuration).FadeOut(400); + } + protected override void LoadComplete() { base.LoadComplete(); - this.FadeInFromZero(100, Easing.OutQuint); + this.FadeInFromZero(FadeInDuration, Easing.OutQuint); switch (Result.Type) { @@ -85,10 +99,7 @@ namespace osu.Game.Rulesets.Judgements this.Delay(600).FadeOut(200); break; default: - JudgementBody.ScaleTo(0.9f); - JudgementBody.ScaleTo(1, 500, Easing.OutElastic); - - this.Delay(100).FadeOut(400); + ApplyHitAnimations(); break; } diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index 2eea6a237c..dded688e80 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods public override void ApplyToClock(IAdjustableClock clock) { if (clock is IHasPitchAdjust pitchAdjust) - pitchAdjust.PitchAdjust = 0.75; + pitchAdjust.PitchAdjust *= RateAdjust; else base.ApplyToClock(clock); } diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index e59654c60d..9ea9eb76bc 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -2,12 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Timing; +using System.Linq; using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { - public abstract class ModDoubleTime : Mod, IApplicableToClock + public abstract class ModDoubleTime : ModTimeAdjust, IApplicableToClock { public override string Name => "Double Time"; public override string Acronym => "DT"; @@ -15,11 +15,9 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyIncrease; public override string Description => "Zoooooooooom..."; public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModHalfTime), typeof(ModTimeRamp) }; - public virtual void ApplyToClock(IAdjustableClock clock) - { - clock.Rate = 1.5; - } + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModHalfTime)).ToArray(); + + protected override double RateAdjust => 1.5; } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 07cceb6f49..fe26c96214 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -2,12 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Timing; +using System.Linq; using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { - public abstract class ModHalfTime : Mod, IApplicableToClock + public abstract class ModHalfTime : ModTimeAdjust, IApplicableToClock { public override string Name => "Half Time"; public override string Acronym => "HT"; @@ -15,11 +15,9 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyReduction; public override string Description => "Less zoom..."; public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime), typeof(ModTimeRamp) }; - public virtual void ApplyToClock(IAdjustableClock clock) - { - clock.Rate = 0.75; - } + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModDoubleTime)).ToArray(); + + protected override double RateAdjust => 0.75; } } diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index e6bd532f72..a689292ed7 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods public override void ApplyToClock(IAdjustableClock clock) { if (clock is IHasPitchAdjust pitchAdjust) - pitchAdjust.PitchAdjust = 1.5; + pitchAdjust.PitchAdjust *= RateAdjust; else base.ApplyToClock(clock); } diff --git a/osu.Game/Rulesets/Mods/ModTimeAdjust.cs b/osu.Game/Rulesets/Mods/ModTimeAdjust.cs new file mode 100644 index 0000000000..513883f552 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModTimeAdjust.cs @@ -0,0 +1,24 @@ +// 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.Framework.Audio; +using osu.Framework.Timing; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModTimeAdjust : Mod + { + public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) }; + + protected abstract double RateAdjust { get; } + + public virtual void ApplyToClock(IAdjustableClock clock) + { + if (clock is IHasTempoAdjust tempo) + tempo.TempoAdjust *= RateAdjust; + else + clock.Rate *= RateAdjust; + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 4a0ed0f9bb..62407907c1 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModTimeRamp : Mod { - public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime), typeof(ModHalfTime) }; + public override Type[] IncompatibleMods => new[] { typeof(ModTimeAdjust) }; protected abstract double FinalRateAdjustment { get; } } @@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Mods private IAdjustableClock clock; - private IHasPitchAdjust pitchAdjust; - /// /// The point in the beatmap at which the final ramping rate should be reached. /// @@ -39,10 +37,11 @@ namespace osu.Game.Rulesets.Mods public virtual void ApplyToClock(IAdjustableClock clock) { this.clock = clock; - pitchAdjust = (IHasPitchAdjust)clock; - // for preview purposes - pitchAdjust.PitchAdjust = 1.0 + FinalRateAdjustment; + lastAdjust = 1; + + // for preview purposes. during gameplay, Update will overwrite this setting. + applyAdjustment(1); } public virtual void ApplyToBeatmap(Beatmap beatmap) @@ -55,10 +54,36 @@ namespace osu.Game.Rulesets.Mods public virtual void Update(Playfield playfield) { - var absRate = Math.Abs(FinalRateAdjustment); - var adjustment = MathHelper.Clamp(absRate * ((clock.CurrentTime - beginRampTime) / finalRateTime), 0, absRate); + applyAdjustment((clock.CurrentTime - beginRampTime) / finalRateTime); + } - pitchAdjust.PitchAdjust = 1 + Math.Sign(FinalRateAdjustment) * adjustment; + private double lastAdjust = 1; + + /// + /// Adjust the rate along the specified ramp + /// + /// The amount of adjustment to apply (from 0..1). + private void applyAdjustment(double amount) + { + double adjust = 1 + (Math.Sign(FinalRateAdjustment) * MathHelper.Clamp(amount, 0, 1) * Math.Abs(FinalRateAdjustment)); + + switch (clock) + { + case IHasPitchAdjust pitch: + pitch.PitchAdjust /= lastAdjust; + pitch.PitchAdjust *= adjust; + break; + case IHasTempoAdjust tempo: + tempo.TempoAdjust /= lastAdjust; + tempo.TempoAdjust *= adjust; + break; + default: + clock.Rate /= lastAdjust; + clock.Rate *= adjust; + break; + } + + lastAdjust = adjust; } } } diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index 646c5c64e4..174070eb85 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -1,6 +1,8 @@ // 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.Game.Graphics; using osu.Game.Rulesets.Objects; @@ -14,6 +16,9 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Sloooow doooown..."; public override FontAwesome Icon => FontAwesome.fa_chevron_circle_down; public override double ScoreMultiplier => 1.0; + protected override double FinalRateAdjustment => -0.25; + + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray(); } } diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index 9050b5591a..bf9af8a51d 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -1,6 +1,8 @@ // 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.Game.Graphics; using osu.Game.Rulesets.Objects; @@ -14,6 +16,9 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Can you keep up?"; public override FontAwesome Icon => FontAwesome.fa_chevron_circle_up; public override double ScoreMultiplier => 1.0; + protected override double FinalRateAdjustment => 0.5; + + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray(); } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index e397139843..8d6bb8bd3f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -5,7 +5,6 @@ using osuTK; using osu.Game.Rulesets.Objects.Types; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using osu.Game.Beatmaps.Formats; using osu.Game.Audio; @@ -46,9 +45,11 @@ namespace osu.Game.Rulesets.Objects.Legacy { string[] split = text.Split(','); - Vector2 pos = new Vector2((int)Convert.ToSingle(split[0], CultureInfo.InvariantCulture), (int)Convert.ToSingle(split[1], CultureInfo.InvariantCulture)); + Vector2 pos = new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)); - ConvertHitObjectType type = (ConvertHitObjectType)int.Parse(split[3]); + double startTime = Parsing.ParseDouble(split[2]) + Offset; + + ConvertHitObjectType type = (ConvertHitObjectType)Parsing.ParseInt(split[3]); int comboOffset = (int)(type & ConvertHitObjectType.ComboOffset) >> 4; type &= ~ConvertHitObjectType.ComboOffset; @@ -56,7 +57,7 @@ namespace osu.Game.Rulesets.Objects.Legacy bool combo = type.HasFlag(ConvertHitObjectType.NewCombo); type &= ~ConvertHitObjectType.NewCombo; - var soundType = (LegacySoundType)int.Parse(split[4]); + var soundType = (LegacySoundType)Parsing.ParseInt(split[4]); var bankInfo = new SampleBankInfo(); HitObject result = null; @@ -107,7 +108,7 @@ namespace osu.Game.Rulesets.Objects.Legacy } string[] temp = t.Split(':'); - points[pointIndex++] = new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)) - pos; + points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos; } // osu-stable special-cased colinear perfect curves to a CurveType.Linear @@ -116,7 +117,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (points.Length == 3 && pathType == PathType.PerfectCurve && isLinear(points)) pathType = PathType.Linear; - int repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture); + int repeatCount = Parsing.ParseInt(split[6]); if (repeatCount > 9000) throw new ArgumentOutOfRangeException(nameof(repeatCount), @"Repeat count is way too high"); @@ -125,7 +126,7 @@ namespace osu.Game.Rulesets.Objects.Legacy repeatCount = Math.Max(0, repeatCount - 1); if (split.Length > 7) - length = Convert.ToDouble(split[7], CultureInfo.InvariantCulture); + length = Math.Max(0, Parsing.ParseDouble(split[7])); if (split.Length > 10) readCustomSampleBanks(split[10], bankInfo); @@ -184,7 +185,9 @@ namespace osu.Game.Rulesets.Objects.Legacy } else if (type.HasFlag(ConvertHitObjectType.Spinner)) { - result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, Convert.ToDouble(split[5], CultureInfo.InvariantCulture) + Offset); + double endTime = Math.Max(startTime, Parsing.ParseDouble(split[5]) + Offset); + + result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, endTime); if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); @@ -193,12 +196,12 @@ namespace osu.Game.Rulesets.Objects.Legacy { // Note: Hold is generated by BMS converts - double endTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture); + double endTime = Math.Max(startTime, Parsing.ParseDouble(split[2])); if (split.Length > 5 && !string.IsNullOrEmpty(split[5])) { string[] ss = split[5].Split(':'); - endTime = Convert.ToDouble(ss[0], CultureInfo.InvariantCulture); + endTime = Math.Max(startTime, Parsing.ParseDouble(ss[0])); readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo); } @@ -211,7 +214,7 @@ namespace osu.Game.Rulesets.Objects.Legacy return null; } - result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture) + Offset; + result.StartTime = startTime; if (result.Samples.Count == 0) result.Samples = convertSoundType(soundType, bankInfo); @@ -222,8 +225,14 @@ namespace osu.Game.Rulesets.Objects.Legacy } catch (FormatException) { - throw new FormatException("One or more hit objects were malformed."); + Logger.Log("A hitobject could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important); } + catch (OverflowException) + { + Logger.Log("A hitobject could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important); + } + + return null; } private void readCustomSampleBanks(string str, SampleBankInfo bankInfo) @@ -233,8 +242,8 @@ namespace osu.Game.Rulesets.Objects.Legacy string[] split = str.Split(':'); - var bank = (LegacyBeatmapDecoder.LegacySampleBank)int.Parse(split[0]); - var addbank = (LegacyBeatmapDecoder.LegacySampleBank)int.Parse(split[1]); + var bank = (LegacyBeatmapDecoder.LegacySampleBank)Parsing.ParseInt(split[0]); + var addbank = (LegacyBeatmapDecoder.LegacySampleBank)Parsing.ParseInt(split[1]); string stringBank = bank.ToString().ToLowerInvariant(); if (stringBank == @"none") @@ -247,10 +256,10 @@ namespace osu.Game.Rulesets.Objects.Legacy bankInfo.Add = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank; if (split.Length > 2) - bankInfo.CustomSampleBank = int.Parse(split[2]); + bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]); if (split.Length > 3) - bankInfo.Volume = int.Parse(split[3]); + bankInfo.Volume = Math.Max(0, Parsing.ParseInt(split[3])); bankInfo.Filename = split.Length > 4 ? split[4] : null; } diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs new file mode 100644 index 0000000000..7cda7485f9 --- /dev/null +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -0,0 +1,148 @@ +// 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.Collections.Generic; +using osuTK; + +namespace osu.Game.Rulesets.Objects +{ + public static class SliderEventGenerator + { + public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, double? legacyLastTickOffset) + { + // A very lenient maximum length of a slider for ticks to be generated. + // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. + const double max_length = 100000; + + var length = Math.Min(max_length, totalDistance); + tickDistance = MathHelper.Clamp(tickDistance, 0, length); + + var minDistanceFromEnd = velocity * 10; + + yield return new SliderEventDescriptor + { + Type = SliderEventType.Head, + SpanIndex = 0, + SpanStartTime = startTime, + Time = startTime, + PathProgress = 0, + }; + + if (tickDistance != 0) + { + for (var span = 0; span < spanCount; span++) + { + var spanStartTime = startTime + span * spanDuration; + var reversed = span % 2 == 1; + + for (var d = tickDistance; d <= length; d += tickDistance) + { + if (d >= length - minDistanceFromEnd) + break; + + var pathProgress = d / length; + var timeProgress = reversed ? 1 - pathProgress : pathProgress; + + yield return new SliderEventDescriptor + { + Type = SliderEventType.Tick, + SpanIndex = span, + SpanStartTime = spanStartTime, + Time = spanStartTime + timeProgress * spanDuration, + PathProgress = pathProgress, + }; + } + + if (span < spanCount - 1) + { + yield return new SliderEventDescriptor + { + Type = SliderEventType.Repeat, + SpanIndex = span, + SpanStartTime = startTime + span * spanDuration, + Time = spanStartTime + spanDuration, + PathProgress = (span + 1) % 2, + }; + } + } + } + + double totalDuration = spanCount * spanDuration; + + // Okay, I'll level with you. I made a mistake. It was 2007. + // Times were simpler. osu! was but in its infancy and sliders were a new concept. + // A hack was made, which has unfortunately lived through until this day. + // + // This legacy tick is used for some calculations and judgements where audio output is not required. + // Generally we are keeping this around just for difficulty compatibility. + // Optimistically we do not want to ever use this for anything user-facing going forwards. + + int finalSpanIndex = spanCount - 1; + double finalSpanStartTime = startTime + finalSpanIndex * spanDuration; + double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) - (legacyLastTickOffset ?? 0)); + double finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration; + + if (spanCount % 2 == 0) finalProgress = 1 - finalProgress; + + yield return new SliderEventDescriptor + { + Type = SliderEventType.LegacyLastTick, + SpanIndex = finalSpanIndex, + SpanStartTime = finalSpanStartTime, + Time = finalSpanEndTime, + PathProgress = finalProgress, + }; + + yield return new SliderEventDescriptor + { + Type = SliderEventType.Tail, + SpanIndex = finalSpanIndex, + SpanStartTime = startTime + (spanCount - 1) * spanDuration, + Time = startTime + totalDuration, + PathProgress = spanCount % 2, + }; + } + } + + /// + /// Describes a point in time on a slider given special meaning. + /// Should be used by rulesets to visualise the slider. + /// + public struct SliderEventDescriptor + { + /// + /// The type of event. + /// + public SliderEventType Type; + + /// + /// The time of this event. + /// + public double Time; + + /// + /// The zero-based index of the span. In the case of repeat sliders, this will increase after each . + /// + public int SpanIndex; + + /// + /// The time at which the contained begins. + /// + public double SpanStartTime; + + /// + /// The progress along the slider's at which this event occurs. + /// + public double PathProgress; + } + + public enum SliderEventType + { + Tick, + LegacyLastTick, + Head, + Tail, + Repeat + } +} diff --git a/osu.Game/Screens/Multi/Components/ParticipantCount.cs b/osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/ParticipantCount.cs rename to osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 594df63420..3c2cec1d94 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -141,11 +142,15 @@ namespace osu.Game.Screens.Play { if (sourceClock == null) return; - sourceClock.Rate = 1; + sourceClock.ResetSpeedAdjustments(); + + if (sourceClock is IHasTempoAdjust tempo) + tempo.TempoAdjust = UserPlaybackRate.Value; + else + sourceClock.Rate = UserPlaybackRate.Value; + foreach (var mod in beatmap.Mods.Value.OfType()) mod.ApplyToClock(sourceClock); - - sourceClock.Rate *= UserPlaybackRate.Value; } } } diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index ca4cce8929..50bc34726a 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -92,30 +92,6 @@ namespace osu.Game.Screens.Play.HUD public Action HoverGained; public Action HoverLost; - public bool OnPressed(GlobalAction action) - { - switch (action) - { - case GlobalAction.Back: - BeginConfirm(); - return true; - } - - return false; - } - - public bool OnReleased(GlobalAction action) - { - switch (action) - { - case GlobalAction.Back: - AbortConfirm(); - return true; - } - - return false; - } - [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -178,7 +154,7 @@ namespace osu.Game.Screens.Play.HUD // avoid starting a new confirm call until we finish animating. pendingAnimation = true; - Progress.Value = 0; + AbortConfirm(); overlayCircle.ScaleTo(0, 100) .Then().FadeOut().ScaleTo(1).FadeIn(500) @@ -207,6 +183,31 @@ namespace osu.Game.Screens.Play.HUD base.OnHoverLost(e); } + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Back: + if (!pendingAnimation) + BeginConfirm(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) + { + switch (action) + { + case GlobalAction.Back: + AbortConfirm(); + return true; + } + + return false; + } + protected override bool OnMouseDown(MouseDownEvent e) { if (!pendingAnimation && e.CurrentState.Mouse.Buttons.Count() == 1) diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 604d7a132b..e8ef94c101 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -175,21 +175,22 @@ namespace osu.Game.Screens.Select private void updateStatistics() { - if (Beatmap == null) + advanced.Beatmap = Beatmap; + description.Text = Beatmap?.Version; + source.Text = Beatmap?.Metadata?.Source; + tags.Text = Beatmap?.Metadata?.Tags; + + // metrics may have been previously fetched + if (Beatmap?.Metrics != null) { - clearStats(); + updateMetrics(Beatmap.Metrics); return; } - ratingsContainer.FadeIn(transition_duration); - advanced.Beatmap = Beatmap; - description.Text = Beatmap.Version; - source.Text = Beatmap.Metadata.Source; - tags.Text = Beatmap.Metadata.Tags; - - var requestedBeatmap = Beatmap; - if (requestedBeatmap.Metrics == null) + // metrics may not be fetched but can be + if (Beatmap?.OnlineBeatmapID != null) { + var requestedBeatmap = Beatmap; var lookup = new GetBeatmapDetailsRequest(requestedBeatmap); lookup.Success += res => { @@ -198,39 +199,34 @@ namespace osu.Game.Screens.Select return; requestedBeatmap.Metrics = res; - Schedule(() => displayMetrics(res)); + Schedule(() => updateMetrics(res)); }; - lookup.Failure += e => Schedule(() => displayMetrics(null)); - + lookup.Failure += e => Schedule(() => updateMetrics()); api.Queue(lookup); loading.Show(); + return; } - displayMetrics(requestedBeatmap.Metrics, false); + updateMetrics(); } - private void displayMetrics(BeatmapMetrics metrics, bool failOnMissing = true) + private void updateMetrics(BeatmapMetrics metrics = null) { var hasRatings = metrics?.Ratings?.Any() ?? false; var hasRetriesFails = (metrics?.Retries?.Any() ?? false) && (metrics.Fails?.Any() ?? false); - if (failOnMissing) loading.Hide(); - if (hasRatings) { ratings.Metrics = metrics; - ratings.FadeIn(transition_duration); + ratingsContainer.FadeIn(transition_duration); } - else if (failOnMissing) + else { ratings.Metrics = new BeatmapMetrics { Ratings = new int[10], }; - } - else - { - ratings.FadeTo(0.25f, transition_duration); + ratingsContainer.FadeTo(0.25f, transition_duration); } if (hasRetriesFails) @@ -238,41 +234,17 @@ namespace osu.Game.Screens.Select failRetryGraph.Metrics = metrics; failRetryContainer.FadeIn(transition_duration); } - else if (failOnMissing) + else { failRetryGraph.Metrics = new BeatmapMetrics { Fails = new int[100], Retries = new int[100], }; + failRetryContainer.FadeOut(transition_duration); } - else - { - failRetryContainer.FadeTo(0.25f, transition_duration); - } - } - - private void clearStats() - { - description.Text = null; - source.Text = null; - tags.Text = null; - - advanced.Beatmap = new BeatmapInfo - { - StarDifficulty = 0, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 0, - DrainRate = 0, - OverallDifficulty = 0, - ApproachRate = 0, - }, - }; loading.Hide(); - ratingsContainer.FadeOut(transition_duration); - failRetryContainer.FadeOut(transition_duration); } private class DetailBox : Container diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestCase.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestCase.cs index 14b4af1e7d..3b3adb4d76 100644 --- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestCase.cs +++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestCase.cs @@ -13,7 +13,7 @@ namespace osu.Game.Tests.Visual base.Update(); // note that this will override any mod rate application - Beatmap.Value.Track.Rate = Clock.Rate; + Beatmap.Value.Track.TempoAdjust = Clock.Rate; } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 2e945c212d..e2c96effb6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -16,7 +16,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index b25e2a8bb2..288630d1ec 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -105,8 +105,8 @@ - - + + diff --git a/osu.iOS/Application.cs b/osu.iOS/Application.cs index 8a5cfcdbe8..cb75e5c159 100644 --- a/osu.iOS/Application.cs +++ b/osu.iOS/Application.cs @@ -13,4 +13,3 @@ namespace osu.iOS } } } - diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index b1ac31f2ce..71cbd83e3e 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -111,6 +111,7 @@ HINT WARNING WARNING + HINT WARNING WARNING WARNING