diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 04708a5ece..82b5492de6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osuTK; @@ -108,16 +109,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void LoadSamples() { - base.LoadSamples(); + // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being. - var firstSample = HitObject.Samples.FirstOrDefault(); - - if (firstSample != null) + if (HitObject.SampleControlPoint == null) { - var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide"); - - slidingSample.Samples = new ISampleInfo[] { clone }; + throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." + + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } + + Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); + + var slidingSamples = new List(); + + var normalSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); + if (normalSample != null) + slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide")); + + var whistleSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE); + if (whistleSample != null) + slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle")); + + slidingSample.Samples = slidingSamples.ToArray(); } public override void StopAllSamples() diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index e2b6c84896..8ba9597dc3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -81,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.Objects public List> NodeSamples { get; set; } = new List>(); + [JsonIgnore] + public IList TailSamples { get; private set; } + private int repeatCount; public int RepeatCount @@ -143,11 +146,6 @@ namespace osu.Game.Rulesets.Osu.Objects Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; - - // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. - // For now, the samples are attached to and played by the slider itself at the correct end time. - // ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live). - Samples = this.GetNodeSamples(repeatCount + 1).ToArray(); } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) @@ -238,6 +236,10 @@ namespace osu.Game.Rulesets.Osu.Objects if (HeadCircle != null) HeadCircle.Samples = this.GetNodeSamples(0); + + // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. + // For now, the samples are played by the slider itself at the correct end time. + TailSamples = this.GetNodeSamples(repeatCount + 1); } public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement(); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index da44b96ed3..acbf57d25f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -273,7 +273,7 @@ namespace osu.Game.Beatmaps.Formats if (hitObject is IHasPath path) { addPathData(writer, path, position); - writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); + writer.Write(getSampleBank(hitObject.Samples)); } else { @@ -420,15 +420,15 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{endTimeData.EndTime}{suffix}")); } - private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false) + private string getSampleBank(IList samples, bool banksOnly = false) { LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank); StringBuilder sb = new StringBuilder(); - sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)normalBank)}:")); - sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)addBank)}")); + sb.Append(FormattableString.Invariant($"{(int)normalBank}:")); + sb.Append(FormattableString.Invariant($"{(int)addBank}")); if (!banksOnly) {