From 551380dd4218b7b194b26762e8f0f75dd81c0d37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Mar 2019 13:48:45 +0900 Subject: [PATCH 01/19] Extract slider tick creation so it can be shared with osu!catch --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 163 +++++++----------- .../Beatmaps/Formats/SliderEventGenerator.cs | 115 ++++++++++++ 2 files changed, 175 insertions(+), 103 deletions(-) create mode 100644 osu.Game/Beatmaps/Formats/SliderEventGenerator.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 345f599b9d..b23be96b93 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; @@ -12,6 +11,7 @@ using osu.Framework.Caching; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; @@ -155,116 +155,73 @@ 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.StartTime, + Position = Position + Path.PositionAt(e.PathProgress), + StackHeight = StackHeight, + Scale = Scale, + Samples = sampleList + }); + break; + case SliderEventType.Head: + AddNested(HeadCircle = new SliderCircle + { + StartTime = e.StartTime, + Position = Position, + Samples = getNodeSamples(0), + SampleControlPoint = SampleControlPoint, + IndexInCurrentCombo = IndexInCurrentCombo, + ComboIndex = ComboIndex, + }); + break; + case SliderEventType.Tail: + AddNested(TailCircle = new SliderTailCircle(this) + { + StartTime = e.StartTime, + 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.SpanIndex + 1) % 2), + StackHeight = StackHeight, + Scale = Scale, + Samples = getNodeSamples(1 + e.SpanIndex) + }); + 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/Beatmaps/Formats/SliderEventGenerator.cs b/osu.Game/Beatmaps/Formats/SliderEventGenerator.cs new file mode 100644 index 0000000000..afe46f42f3 --- /dev/null +++ b/osu.Game/Beatmaps/Formats/SliderEventGenerator.cs @@ -0,0 +1,115 @@ +// 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.Beatmaps.Formats +{ + public static class SliderEventGenerator + { + public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, double? legacyLastTickOffset) + { + List events = new List(); + + // 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; + + events.Add(new SliderEventDescriptor + { + Type = SliderEventType.Head, + SpanIndex = 0, + SpanStartTime = startTime, + StartTime = 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; + + events.Add(new SliderEventDescriptor + { + Type = SliderEventType.Tick, + SpanIndex = span, + SpanStartTime = spanStartTime, + StartTime = spanStartTime + timeProgress * spanDuration, + PathProgress = pathProgress, + }); + } + + if (span < spanCount - 1) + { + events.Add(new SliderEventDescriptor + { + Type = SliderEventType.Repeat, + SpanIndex = span, + SpanStartTime = startTime + span * spanDuration, + StartTime = spanStartTime + (span + 1) * spanDuration, + PathProgress = 1, + }); + } + } + } + + double totalDuration = spanCount * spanDuration; + + var tail = new SliderEventDescriptor + { + Type = SliderEventType.Tail, + SpanIndex = spanCount - 1, + SpanStartTime = startTime + (spanCount - 1) * spanDuration, + StartTime = startTime + totalDuration, + PathProgress = 1, + }; + + if (legacyLastTickOffset != null) + tail.StartTime = Math.Max(startTime + totalDuration / 2, tail.StartTime - legacyLastTickOffset.Value); + + events.Add(tail); + + return events; + } + } + } + + public class SliderEventDescriptor + { + public SliderEventType Type; + + public int SpanIndex; + + public double SpanStartTime; + + public double StartTime; + + public double PathProgress; + } + + public enum SliderEventType + { + Tick, + Head, + Tail, + Repeat + } +} From 973f29b7653a5e57e17be02b268f9ac07c8076c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Mar 2019 15:14:57 +0900 Subject: [PATCH 02/19] Apply review --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 3 +- .../Beatmaps/Formats/SliderEventGenerator.cs | 115 ------------------ .../Rulesets/Objects/SliderEventGenerator.cs | 109 +++++++++++++++++ 3 files changed, 110 insertions(+), 117 deletions(-) delete mode 100644 osu.Game/Beatmaps/Formats/SliderEventGenerator.cs create mode 100644 osu.Game/Rulesets/Objects/SliderEventGenerator.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index b23be96b93..9638d5e6f0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -11,7 +11,6 @@ using osu.Framework.Caching; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; @@ -210,7 +209,7 @@ namespace osu.Game.Rulesets.Osu.Objects RepeatIndex = e.SpanIndex, SpanDuration = SpanDuration, StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, - Position = Position + Path.PositionAt((e.SpanIndex + 1) % 2), + Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, Scale = Scale, Samples = getNodeSamples(1 + e.SpanIndex) diff --git a/osu.Game/Beatmaps/Formats/SliderEventGenerator.cs b/osu.Game/Beatmaps/Formats/SliderEventGenerator.cs deleted file mode 100644 index afe46f42f3..0000000000 --- a/osu.Game/Beatmaps/Formats/SliderEventGenerator.cs +++ /dev/null @@ -1,115 +0,0 @@ -// 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.Beatmaps.Formats -{ - public static class SliderEventGenerator - { - public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, double? legacyLastTickOffset) - { - List events = new List(); - - // 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; - - events.Add(new SliderEventDescriptor - { - Type = SliderEventType.Head, - SpanIndex = 0, - SpanStartTime = startTime, - StartTime = 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; - - events.Add(new SliderEventDescriptor - { - Type = SliderEventType.Tick, - SpanIndex = span, - SpanStartTime = spanStartTime, - StartTime = spanStartTime + timeProgress * spanDuration, - PathProgress = pathProgress, - }); - } - - if (span < spanCount - 1) - { - events.Add(new SliderEventDescriptor - { - Type = SliderEventType.Repeat, - SpanIndex = span, - SpanStartTime = startTime + span * spanDuration, - StartTime = spanStartTime + (span + 1) * spanDuration, - PathProgress = 1, - }); - } - } - } - - double totalDuration = spanCount * spanDuration; - - var tail = new SliderEventDescriptor - { - Type = SliderEventType.Tail, - SpanIndex = spanCount - 1, - SpanStartTime = startTime + (spanCount - 1) * spanDuration, - StartTime = startTime + totalDuration, - PathProgress = 1, - }; - - if (legacyLastTickOffset != null) - tail.StartTime = Math.Max(startTime + totalDuration / 2, tail.StartTime - legacyLastTickOffset.Value); - - events.Add(tail); - - return events; - } - } - } - - public class SliderEventDescriptor - { - public SliderEventType Type; - - public int SpanIndex; - - public double SpanStartTime; - - public double StartTime; - - public double PathProgress; - } - - public enum SliderEventType - { - Tick, - Head, - Tail, - Repeat - } -} diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs new file mode 100644 index 0000000000..39827ef532 --- /dev/null +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -0,0 +1,109 @@ +// 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, + StartTime = 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, + StartTime = spanStartTime + timeProgress * spanDuration, + PathProgress = pathProgress, + }; + } + + if (span < spanCount - 1) + { + yield return new SliderEventDescriptor + { + Type = SliderEventType.Repeat, + SpanIndex = span, + SpanStartTime = startTime + span * spanDuration, + StartTime = spanStartTime + (span + 1) * spanDuration, + PathProgress = (span + 1) % 2, + }; + } + } + } + + double totalDuration = spanCount * spanDuration; + + var tail = new SliderEventDescriptor + { + Type = SliderEventType.Tail, + SpanIndex = spanCount - 1, + SpanStartTime = startTime + (spanCount - 1) * spanDuration, + StartTime = startTime + totalDuration, + PathProgress = 1, + }; + + if (legacyLastTickOffset != null) + tail.StartTime = Math.Max(startTime + totalDuration / 2, tail.StartTime - legacyLastTickOffset.Value); + + yield return tail; + } + } + + public struct SliderEventDescriptor + { + public SliderEventType Type; + + public int SpanIndex; + + public double SpanStartTime; + + public double StartTime; + + public double PathProgress; + } + + public enum SliderEventType + { + Tick, + Head, + Tail, + Repeat + } +} From 355705f0a5e510d6c4bcd25b8ba86f9f8ffda4ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Mar 2019 19:57:30 +0900 Subject: [PATCH 03/19] Fix legacy tick handling --- .../Objects/JuiceStream.cs | 127 +++++++----------- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- .../Rulesets/Objects/SliderEventGenerator.cs | 35 +++-- 3 files changed, 75 insertions(+), 89 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 61bb4335f3..c5c9b09b89 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,57 @@ 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.StartTime - lastEvent.Value.StartTime; + + 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.StartTime, + 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 + 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.StartTime, + 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.StartTime, + X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, + }); + break; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 9638d5e6f0..4ea8b00c32 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Objects ComboIndex = ComboIndex, }); break; - case SliderEventType.Tail: + case SliderEventType.LegacyFinalTick: AddNested(TailCircle = new SliderTailCircle(this) { StartTime = e.StartTime, diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 39827ef532..3fabe33b6a 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Objects Type = SliderEventType.Repeat, SpanIndex = span, SpanStartTime = startTime + span * spanDuration, - StartTime = spanStartTime + (span + 1) * spanDuration, + StartTime = spanStartTime + spanDuration, PathProgress = (span + 1) % 2, }; } @@ -70,19 +70,37 @@ namespace osu.Game.Rulesets.Objects double totalDuration = spanCount * spanDuration; - var tail = new SliderEventDescriptor + // 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 finalSpanTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) - (legacyLastTickOffset ?? 0)); + double finalProgress = (finalSpanTime - finalSpanStartTime) / spanDuration; + if (spanCount % 2 == 0) finalProgress = 1 - finalProgress; + + yield return new SliderEventDescriptor + { + Type = SliderEventType.LegacyFinalTick, + SpanIndex = finalSpanIndex, + SpanStartTime = finalSpanStartTime, + StartTime = finalSpanTime, + PathProgress = finalProgress, + }; + + yield return new SliderEventDescriptor { Type = SliderEventType.Tail, SpanIndex = spanCount - 1, SpanStartTime = startTime + (spanCount - 1) * spanDuration, StartTime = startTime + totalDuration, - PathProgress = 1, + PathProgress = spanCount % 2, }; - - if (legacyLastTickOffset != null) - tail.StartTime = Math.Max(startTime + totalDuration / 2, tail.StartTime - legacyLastTickOffset.Value); - - yield return tail; } } @@ -102,6 +120,7 @@ namespace osu.Game.Rulesets.Objects public enum SliderEventType { Tick, + LegacyFinalTick, Head, Tail, Repeat From dd50c5dc1a2102cca5f7f967ccb7e4750a5b0c71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Mar 2019 19:57:39 +0900 Subject: [PATCH 04/19] Add player test for osu! ruleset --- osu.Game.Rulesets.Osu.Tests/TestCaseOsuPlayer.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestCaseOsuPlayer.cs 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()) + { + } + } +} From 165a353a83b7daacf5ed913b004ab64982ef2069 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Mar 2019 20:12:48 +0900 Subject: [PATCH 05/19] Add extensive commenting about LegacyLastTick usage --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 2 ++ osu.Game.Rulesets.Osu/Objects/Slider.cs | 6 ++++-- osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index c5c9b09b89..9a5bad01fe 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Catch.Objects } } + // 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) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 4ea8b00c32..5de36e1cff 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.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 osuTK; @@ -194,7 +194,9 @@ namespace osu.Game.Rulesets.Osu.Objects ComboIndex = ComboIndex, }); break; - case SliderEventType.LegacyFinalTick: + // 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.StartTime, 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(); From 93a999396e24f00bf048fb705365d28bcddb34c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Mar 2019 20:13:11 +0900 Subject: [PATCH 06/19] LegacyFinalTick -> LegacyLastTick to match existing variable --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 3 ++- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 5de36e1cff..1efd92987d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.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 osuTK; @@ -194,6 +194,7 @@ namespace osu.Game.Rulesets.Osu.Objects 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. diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 3fabe33b6a..3f8927e740 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Objects yield return new SliderEventDescriptor { - Type = SliderEventType.LegacyFinalTick, + Type = SliderEventType.LegacyLastTick, SpanIndex = finalSpanIndex, SpanStartTime = finalSpanStartTime, StartTime = finalSpanTime, @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Objects public enum SliderEventType { Tick, - LegacyFinalTick, + LegacyLastTick, Head, Tail, Repeat From eadd7ce9138403f189d9ad06871dcce3c1d2d93f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Mar 2019 20:13:55 +0900 Subject: [PATCH 07/19] Update catch dfificulty test Likely the result of fixing https://github.com/ppy/osu/issues/4426. --- osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 7311ccabfbb18f9d53b5a695381022b3e05151e1 Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 8 Mar 2019 20:27:50 -0800 Subject: [PATCH 08/19] Fix direct panel stats being misaligned by one pixel --- osu.Game/Overlays/Direct/DirectGridPanel.cs | 5 +---- osu.Game/Overlays/Direct/DirectListPanel.cs | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) 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 { From 0fc6fa7245598b4b120c91b067e42e474ddb10f4 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 9 Mar 2019 20:29:56 -0800 Subject: [PATCH 09/19] Fix file naming on ParticipantCountDisplay --- .../{ParticipantCount.cs => ParticipantCountDisplay.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game/Screens/Multi/Components/{ParticipantCount.cs => ParticipantCountDisplay.cs} (100%) 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 From 2029cf93fd1962f8f0b01161d50264cd2a85575a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2019 14:33:21 +0900 Subject: [PATCH 10/19] Rename and reuse variables --- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 3f8927e740..84621ba7a3 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -80,8 +80,9 @@ namespace osu.Game.Rulesets.Objects int finalSpanIndex = spanCount - 1; double finalSpanStartTime = startTime + finalSpanIndex * spanDuration; - double finalSpanTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) - (legacyLastTickOffset ?? 0)); - double finalProgress = (finalSpanTime - finalSpanStartTime) / 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 @@ -89,14 +90,14 @@ namespace osu.Game.Rulesets.Objects Type = SliderEventType.LegacyLastTick, SpanIndex = finalSpanIndex, SpanStartTime = finalSpanStartTime, - StartTime = finalSpanTime, + StartTime = finalSpanEndTime, PathProgress = finalProgress, }; yield return new SliderEventDescriptor { Type = SliderEventType.Tail, - SpanIndex = spanCount - 1, + SpanIndex = finalSpanIndex, SpanStartTime = startTime + (spanCount - 1) * spanDuration, StartTime = startTime + totalDuration, PathProgress = spanCount % 2, From 489153579a7b8a15d5406485005b9d7ee80c2d7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2019 14:36:29 +0900 Subject: [PATCH 11/19] Add xmldoc and clarify struct variables --- .../Objects/JuiceStream.cs | 8 ++--- osu.Game.Rulesets.Osu/Objects/Slider.cs | 6 ++-- .../Rulesets/Objects/SliderEventGenerator.cs | 33 +++++++++++++++---- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 9a5bad01fe..2adc156efd 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Objects // generate tiny droplets since the last point if (lastEvent != null) { - double sinceLastTick = e.StartTime - lastEvent.Value.StartTime; + double sinceLastTick = e.Time - lastEvent.Value.Time; if (sinceLastTick > 80) { @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new TinyDroplet { Samples = tickSamples, - StartTime = t + lastEvent.Value.StartTime, + StartTime = t + lastEvent.Value.Time, X = X + Path.PositionAt( lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH, }); @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new Droplet { Samples = tickSamples, - StartTime = e.StartTime, + StartTime = e.Time, X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, }); break; @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new Fruit { Samples = Samples, - StartTime = e.StartTime, + StartTime = e.Time, X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, }); break; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 1efd92987d..4a2b3ecb2d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Objects { SpanIndex = e.SpanIndex, SpanStartTime = e.SpanStartTime, - StartTime = e.StartTime, + StartTime = e.Time, Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, Scale = Scale, @@ -186,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Objects case SliderEventType.Head: AddNested(HeadCircle = new SliderCircle { - StartTime = e.StartTime, + StartTime = e.Time, Position = Position, Samples = getNodeSamples(0), SampleControlPoint = SampleControlPoint, @@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Objects // if this is to change, we should revisit this. AddNested(TailCircle = new SliderTailCircle(this) { - StartTime = e.StartTime, + StartTime = e.Time, Position = EndPosition, IndexInCurrentCombo = IndexInCurrentCombo, ComboIndex = ComboIndex, diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 84621ba7a3..99ad78eed1 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Objects Type = SliderEventType.Head, SpanIndex = 0, SpanStartTime = startTime, - StartTime = startTime, + Time = startTime, PathProgress = 0, }; @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Objects Type = SliderEventType.Tick, SpanIndex = span, SpanStartTime = spanStartTime, - StartTime = spanStartTime + timeProgress * spanDuration, + Time = spanStartTime + timeProgress * spanDuration, PathProgress = pathProgress, }; } @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Objects Type = SliderEventType.Repeat, SpanIndex = span, SpanStartTime = startTime + span * spanDuration, - StartTime = spanStartTime + spanDuration, + Time = spanStartTime + spanDuration, PathProgress = (span + 1) % 2, }; } @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Objects Type = SliderEventType.LegacyLastTick, SpanIndex = finalSpanIndex, SpanStartTime = finalSpanStartTime, - StartTime = finalSpanEndTime, + Time = finalSpanEndTime, PathProgress = finalProgress, }; @@ -99,22 +99,41 @@ namespace osu.Game.Rulesets.Objects Type = SliderEventType.Tail, SpanIndex = finalSpanIndex, SpanStartTime = startTime + (spanCount - 1) * spanDuration, - StartTime = startTime + totalDuration, + 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 each repeat. + /// public int SpanIndex; + /// + /// The time at which the contained begins. + /// public double SpanStartTime; - public double StartTime; - + /// + /// The progress along the slider's at which this event occurs. + /// public double PathProgress; } From 63fea65c0c1f25aa77fcee286074ec2441fdbdd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2019 14:53:21 +0900 Subject: [PATCH 12/19] Clarify repeat index --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 4a2b3ecb2d..1afbacc01e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Objects Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, Scale = Scale, - Samples = getNodeSamples(1 + e.SpanIndex) + Samples = getNodeSamples(e.SpanIndex + 1) }); break; } diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 99ad78eed1..a0f9d0a481 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Objects public double Time; /// - /// The zero-based index of the span. In the case of repeat sliders, this will increase each repeat. + /// The zero-based index of the span. In the case of repeat sliders, this will increase after each . /// public int SpanIndex; From 3a8c32d41b83cf980eb778ab8a9162c32586ae0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2019 17:03:01 +0900 Subject: [PATCH 13/19] Add the ability for ArchiveModelManager to re-import even when existing entry is present --- osu.Game/Beatmaps/BeatmapManager.cs | 12 ++++++ osu.Game/Database/ArchiveModelManager.cs | 51 ++++++++++++++++++------ 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 88f5e777e3..974c023a20 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -106,7 +106,10 @@ namespace osu.Game.Beatmaps 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) { @@ -254,6 +257,15 @@ 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; + + // force re-import if we are not in a sane state. + return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID; + } + /// /// Returns a list of all usable s. /// 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(); From d0ae75af6e9ef743f20d84f84f4466445e6aca08 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2019 18:13:33 +0900 Subject: [PATCH 14/19] Add tests and fix scenario where all matching are contained by duplicate candidate --- .../Beatmaps/IO/ImportBeatmapTest.cs | 35 +++++++++++++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 35 ++++++++++++++----- 2 files changed, 62 insertions(+), 8 deletions(-) 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/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 974c023a20..711aa0b79b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -102,7 +102,7 @@ namespace osu.Game.Beatmaps b.BeatmapSet = beatmapSet; } - validateOnlineIds(beatmapSet.Beatmaps); + validateOnlineIds(beatmapSet); foreach (BeatmapInfo b in beatmapSet.Beatmaps) fetchAndPopulateOnlineValues(b, beatmapSet.Beatmaps); @@ -123,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); } /// @@ -262,8 +278,11 @@ namespace osu.Game.Beatmaps 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; + return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds); } /// From 59897bbeb5607ef5d96763bea87425545b04f6c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2019 18:45:30 +0900 Subject: [PATCH 15/19] Refactor UpdateableBeatmapBackground lookup logic for clarity Closes #4401. Alternative to #4439. --- .../UpdateableBeatmapBackgroundSprite.cs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) 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); + } } } From 941a72d69aa9b7c688885f89c9a51c3cf067101c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Mar 2019 01:34:31 +0900 Subject: [PATCH 16/19] Fix osu!direct firing excess queries during initial search characters Due to faulty debounce fire logic, a web request would always fire with a single character search, followed by the real search. This caused unwanted delays and display weirdness. --- osu.Game/Overlays/DirectOverlay.cs | 48 +++++++++++++----------------- 1 file changed, 20 insertions(+), 28 deletions(-) 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 (?) From 38e75421ab663e63343b9d1121541c2358d1b2b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Mar 2019 02:21:34 +0900 Subject: [PATCH 17/19] Fix HoldForMenuButton getting stuck in a confirming state Closes #4446. --- .../Screens/Play/HUD/HoldForMenuButton.cs | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index ca4cce8929..8bef15a9f1 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,32 @@ 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) From 1be4c7b813d423360d25e3eef3de857add501ded Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Mar 2019 02:23:23 +0900 Subject: [PATCH 18/19] Fix excess newline --- osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 8bef15a9f1..50bc34726a 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -208,7 +208,6 @@ namespace osu.Game.Screens.Play.HUD return false; } - protected override bool OnMouseDown(MouseDownEvent e) { if (!pendingAnimation && e.CurrentState.Mouse.Buttons.Count() == 1) From ad3b956cec4fdfad8588a30a5c62846f9ae5337d Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 11 Mar 2019 20:10:59 -0700 Subject: [PATCH 19/19] Fix channel selection overlay dimming bg forever when popped out --- osu.Game/Overlays/ChatOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) 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(); }