From 730b14b5bb102572c742b769e168c48febd9791c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 19:51:09 +0900 Subject: [PATCH 01/48] Add initial hit sample pooling --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 13 +- osu.Game/Audio/HitSampleInfo.cs | 15 +- osu.Game/Audio/SampleInfo.cs | 15 +- .../Objects/Drawables/DrawableHitObject.cs | 12 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 29 ++- osu.Game/Rulesets/UI/Playfield.cs | 38 +++- osu.Game/Skinning/PausableSkinnableSound.cs | 8 +- osu.Game/Skinning/SkinReloadableDrawable.cs | 4 +- osu.Game/Skinning/SkinnableSound.cs | 203 +++++++++++++++--- .../Drawables/DrawableStoryboardSample.cs | 4 +- 10 files changed, 283 insertions(+), 58 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 4ecfb7b16d..d61a88b7df 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Game.Audio; using osu.Game.Rulesets.Catch.Judgements; @@ -26,11 +27,17 @@ namespace osu.Game.Rulesets.Catch.Objects Samples = samples; } - private class BananaHitSampleInfo : HitSampleInfo + private class BananaHitSampleInfo : HitSampleInfo, IEquatable { - private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; + private static readonly string[] lookup_names = { "metronomelow", "catch-banana" }; - public override IEnumerable LookupNames => lookupNames; + public override IEnumerable LookupNames => lookup_names; + + public bool Equals(BananaHitSampleInfo other) => true; + + public override bool Equals(object obj) => obj is BananaHitSampleInfo other && Equals(other); + + public override int GetHashCode() => lookup_names.GetHashCode(); } } } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 8efaeb3795..46f0abd7b7 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace osu.Game.Audio { @@ -10,7 +11,7 @@ namespace osu.Game.Audio /// Describes a gameplay hit sample. /// [Serializable] - public class HitSampleInfo : ISampleInfo + public class HitSampleInfo : ISampleInfo, IEquatable { public const string HIT_WHISTLE = @"hitwhistle"; public const string HIT_FINISH = @"hitfinish"; @@ -57,5 +58,17 @@ namespace osu.Game.Audio } public HitSampleInfo Clone() => (HitSampleInfo)MemberwiseClone(); + + public bool Equals(HitSampleInfo other) + => other != null && Bank == other.Bank && Name == other.Name && Suffix == other.Suffix; + + public override bool Equals(object obj) + => obj is HitSampleInfo other && Equals(other); + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] // This will have to be addressed eventually + public override int GetHashCode() + { + return HashCode.Combine(Bank, Name, Suffix); + } } } diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index 240d70c418..221bc31639 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -1,14 +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 System; using System.Collections.Generic; +using System.Linq; namespace osu.Game.Audio { /// /// Describes a gameplay sample. /// - public class SampleInfo : ISampleInfo + public class SampleInfo : ISampleInfo, IEquatable { private readonly string[] sampleNames; @@ -20,5 +22,16 @@ namespace osu.Game.Audio public IEnumerable LookupNames => sampleNames; public int Volume { get; } = 100; + + public override int GetHashCode() + { + return HashCode.Combine(sampleNames, Volume); + } + + public bool Equals(SampleInfo other) + => other != null && sampleNames.SequenceEqual(other.sampleNames); + + public override bool Equals(object obj) + => obj is SampleInfo other && Equals(other); } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ca49ed9e75..0d97066b35 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -10,7 +10,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Logging; using osu.Framework.Threading; @@ -139,8 +138,6 @@ namespace osu.Game.Rulesets.Objects.Drawables [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } - private Container samplesContainer; - /// /// Creates a new . /// @@ -159,7 +156,7 @@ namespace osu.Game.Rulesets.Objects.Drawables config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); // Explicit non-virtual function call. - base.AddInternal(samplesContainer = new Container { RelativeSizeAxes = Axes.Both }); + base.AddInternal(Samples = new PausableSkinnableSound(Array.Empty())); } protected override void LoadAsyncComplete() @@ -269,6 +266,8 @@ namespace osu.Game.Rulesets.Objects.Drawables // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). samplesBindable.CollectionChanged -= onSamplesChanged; + Samples.Samples = Array.Empty(); + if (nestedHitObjects.IsValueCreated) { foreach (var obj in nestedHitObjects.Value) @@ -335,8 +334,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual void LoadSamples() { - samplesContainer.Clear(); - Samples = null; + Samples.Samples = Array.Empty(); var samples = GetSamples().ToArray(); @@ -349,7 +347,7 @@ namespace osu.Game.Rulesets.Objects.Drawables + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } - samplesContainer.Add(Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)))); + Samples.Samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); } private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 44b22033dc..19d573a55a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -5,6 +5,7 @@ using osuTK; using osu.Game.Rulesets.Objects.Types; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using osu.Game.Beatmaps.Formats; using osu.Game.Audio; @@ -500,7 +501,7 @@ namespace osu.Game.Rulesets.Objects.Legacy public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); } - public class LegacyHitSampleInfo : HitSampleInfo + public class LegacyHitSampleInfo : HitSampleInfo, IEquatable { private int customSampleBank; @@ -524,9 +525,21 @@ namespace osu.Game.Rulesets.Objects.Legacy /// using the skin config option. /// public bool IsLayered { get; set; } + + public bool Equals(LegacyHitSampleInfo other) + => other != null && base.Equals(other) && CustomSampleBank == other.CustomSampleBank; + + public override bool Equals(object obj) + => obj is LegacyHitSampleInfo other && Equals(other); + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] // This will have to be addressed eventually + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), customSampleBank); + } } - private class FileHitSampleInfo : LegacyHitSampleInfo + private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable { public string Filename; @@ -542,6 +555,18 @@ namespace osu.Game.Rulesets.Objects.Legacy Filename, Path.ChangeExtension(Filename, null) }; + + public bool Equals(FileHitSampleInfo other) + => other != null && Filename == other.Filename; + + public override bool Equals(object obj) + => obj is FileHitSampleInfo other && Equals(other); + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] // This will have to be addressed eventually + public override int GetHashCode() + { + return HashCode.Combine(Filename); + } } } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 82ec653f31..9f3a4c508f 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -8,19 +8,23 @@ using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; +using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.UI { [Cached(typeof(IPooledHitObjectProvider))] - public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider + [Cached(typeof(IPooledSampleProvider))] + public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider { /// /// Invoked when a is judged. @@ -80,6 +84,12 @@ namespace osu.Game.Rulesets.UI /// public readonly BindableBool DisplayJudgements = new BindableBool(true); + [Resolved(CanBeNull = true)] + private IReadOnlyList mods { get; set; } + + [Resolved] + private ISampleStore sampleStore { get; set; } + /// /// Creates a new . /// @@ -96,9 +106,6 @@ namespace osu.Game.Rulesets.UI })); } - [Resolved(CanBeNull = true)] - private IReadOnlyList mods { get; set; } - [BackgroundDependencyLoader] private void load() { @@ -336,6 +343,29 @@ namespace osu.Game.Rulesets.UI }); } + private readonly Dictionary> samplePools = new Dictionary>(); + + public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) + { + if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) + samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 5); + + return existingPool.Get(); + } + + private class DrawableSamplePool : DrawablePool + { + private readonly ISampleInfo sampleInfo; + + public DrawableSamplePool(ISampleInfo sampleInfo, int initialSize, int? maximumSize = null) + : base(initialSize, maximumSize) + { + this.sampleInfo = sampleInfo; + } + + protected override PoolableSkinnableSample CreateNewDrawable() => base.CreateNewDrawable().With(d => d.Apply(sampleInfo)); + } + #endregion #region Editor logic diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 4f09aec0b6..758b784649 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -14,13 +14,13 @@ namespace osu.Game.Skinning { protected bool RequestedPlaying { get; private set; } - public PausableSkinnableSound(ISampleInfo hitSamples) - : base(hitSamples) + public PausableSkinnableSound(ISampleInfo sample) + : base(sample) { } - public PausableSkinnableSound(IEnumerable hitSamples) - : base(hitSamples) + public PausableSkinnableSound(IEnumerable samples) + : base(samples) { } diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index cc9cbf7b59..50b4143375 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -27,7 +27,7 @@ namespace osu.Game.Skinning /// /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. /// - private bool allowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin); + protected bool AllowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin); /// /// Create a new @@ -58,7 +58,7 @@ namespace osu.Game.Skinning private void skinChanged() { - SkinChanged(CurrentSkin, allowDefaultFallback); + SkinChanged(CurrentSkin, AllowDefaultFallback); OnSkinChanged?.Invoke(); } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index ffa0a963ce..8410b7eeae 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -1,26 +1,149 @@ // 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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Game.Audio; namespace osu.Game.Skinning { - public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent + public interface IPooledSampleProvider { - private readonly ISampleInfo[] hitSamples; + [CanBeNull] + PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); + } + + public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent + { + private ISampleInfo sampleInfo; + private DrawableSample sample; [Resolved] - private ISampleStore samples { get; set; } + private ISampleStore sampleStore { get; set; } + [Cached] + private readonly AudioAdjustments adjustments = new AudioAdjustments(); + + public PoolableSkinnableSample() + { + } + + public PoolableSkinnableSample(ISampleInfo sampleInfo) + { + Apply(sampleInfo); + } + + public void Apply(ISampleInfo sampleInfo) + { + if (this.sampleInfo != null) + throw new InvalidOperationException($"A {nameof(PoolableSkinnableSample)} cannot be applied multiple {nameof(ISampleInfo)}s."); + + this.sampleInfo = sampleInfo; + + if (LoadState >= LoadState.Ready) + updateSample(); + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateSample(); + } + + private void updateSample() + { + ClearInternal(); + + var ch = CurrentSkin.GetSample(sampleInfo); + + if (ch == null && AllowDefaultFallback) + { + foreach (var lookup in sampleInfo.LookupNames) + { + if ((ch = sampleStore.Get(lookup)) != null) + break; + } + } + + if (ch == null) + return; + + AddInternal(sample = new DrawableSample(ch) + { + Looping = Looping, + Volume = { Value = sampleInfo.Volume / 100.0 } + }); + } + + public void Play(bool restart = true) => sample?.Play(restart); + + public void Stop() => sample?.Stop(); + + public bool Playing => sample?.Playing ?? false; + + private bool looping; + + public bool Looping + { + get => looping; + set + { + looping = value; + + if (sample != null) + sample.Looping = value; + } + } + + /// + /// The volume of this component. + /// + public BindableNumber Volume => adjustments.Volume; + + /// + /// The playback balance of this sample (-1 .. 1 where 0 is centered) + /// + public BindableNumber Balance => adjustments.Balance; + + /// + /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. + /// + public BindableNumber Frequency => adjustments.Frequency; + + /// + /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. + /// + public BindableNumber Tempo => adjustments.Tempo; + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => adjustments.AddAdjustment(type, adjustBindable); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => adjustments.RemoveAdjustment(type, adjustBindable); + + public void RemoveAllAdjustments(AdjustableProperty type) => adjustments.RemoveAllAdjustments(type); + + public IBindable AggregateVolume => adjustments.AggregateVolume; + + public IBindable AggregateBalance => adjustments.AggregateBalance; + + public IBindable AggregateFrequency => adjustments.AggregateFrequency; + + public IBindable AggregateTempo => adjustments.AggregateTempo; + } + + public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent + { public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; @@ -34,17 +157,44 @@ namespace osu.Game.Skinning /// protected bool PlayWhenZeroVolume => Looping; - protected readonly AudioContainer SamplesContainer; + protected readonly AudioContainer SamplesContainer; - public SkinnableSound(ISampleInfo hitSamples) - : this(new[] { hitSamples }) + [Resolved] + private ISampleStore sampleStore { get; set; } + + [Resolved(CanBeNull = true)] + private IPooledSampleProvider pooledProvider { get; set; } + + public SkinnableSound(ISampleInfo sample) + : this(new[] { sample }) { } - public SkinnableSound(IEnumerable hitSamples) + public SkinnableSound(IEnumerable samples) { - this.hitSamples = hitSamples.ToArray(); - InternalChild = SamplesContainer = new AudioContainer(); + this.samples = samples.ToArray(); + + InternalChild = SamplesContainer = new AudioContainer(); + } + + private ISampleInfo[] samples; + + public ISampleInfo[] Samples + { + get => samples; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (samples == value) + return; + + samples = value; + + if (LoadState >= LoadState.Ready) + updateSamples(); + } } private bool looping; @@ -77,34 +227,23 @@ namespace osu.Game.Skinning } protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + // Start playback internally for the new samples if the previous ones were playing beforehand. + if (IsPlaying) + Play(); + } + + private void updateSamples() { bool wasPlaying = IsPlaying; - var channels = hitSamples.Select(s => - { - var ch = skin.GetSample(s); + // Remove all pooled samples (return them to the pool), and dispose the rest. + SamplesContainer.RemoveAll(s => s.IsInPool); + SamplesContainer.Clear(); - if (ch == null && allowFallback) - { - foreach (var lookup in s.LookupNames) - { - if ((ch = samples.Get(lookup)) != null) - break; - } - } + foreach (var s in samples) + SamplesContainer.Add(pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s)); - if (ch != null) - { - ch.Looping = looping; - ch.Volume.Value = s.Volume / 100.0; - } - - return ch; - }).Where(c => c != null); - - SamplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); - - // Start playback internally for the new samples if the previous ones were playing beforehand. if (wasPlaying) Play(); } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 08811b9b8c..b8d212d3c9 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -37,8 +37,8 @@ namespace osu.Game.Storyboards.Drawables foreach (var mod in mods.Value.OfType()) { - foreach (var sample in SamplesContainer) - mod.ApplyToSample(sample); + // foreach (var sample in SamplesContainer) + // mod.ApplyToSample(sample.Sample); } } From 003fed857c9f35c258f6ad1499d8e5bc4e624827 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 19:52:34 +0900 Subject: [PATCH 02/48] Separate files --- osu.Game/Skinning/IPooledSampleProvider.cs | 14 ++ osu.Game/Skinning/PoolableSkinnableSample.cs | 133 +++++++++++++++++++ osu.Game/Skinning/SkinnableSound.cs | 127 ------------------ 3 files changed, 147 insertions(+), 127 deletions(-) create mode 100644 osu.Game/Skinning/IPooledSampleProvider.cs create mode 100644 osu.Game/Skinning/PoolableSkinnableSample.cs diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs new file mode 100644 index 0000000000..3dc0b5375d --- /dev/null +++ b/osu.Game/Skinning/IPooledSampleProvider.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + public interface IPooledSampleProvider + { + [CanBeNull] + PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); + } +} diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs new file mode 100644 index 0000000000..6ad7345954 --- /dev/null +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -0,0 +1,133 @@ +// 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.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent + { + private ISampleInfo sampleInfo; + private DrawableSample sample; + + [Resolved] + private ISampleStore sampleStore { get; set; } + + [Cached] + private readonly AudioAdjustments adjustments = new AudioAdjustments(); + + public PoolableSkinnableSample() + { + } + + public PoolableSkinnableSample(ISampleInfo sampleInfo) + { + Apply(sampleInfo); + } + + public void Apply(ISampleInfo sampleInfo) + { + if (this.sampleInfo != null) + throw new InvalidOperationException($"A {nameof(PoolableSkinnableSample)} cannot be applied multiple {nameof(ISampleInfo)}s."); + + this.sampleInfo = sampleInfo; + + if (LoadState >= LoadState.Ready) + updateSample(); + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateSample(); + } + + private void updateSample() + { + ClearInternal(); + + var ch = CurrentSkin.GetSample(sampleInfo); + + if (ch == null && AllowDefaultFallback) + { + foreach (var lookup in sampleInfo.LookupNames) + { + if ((ch = sampleStore.Get(lookup)) != null) + break; + } + } + + if (ch == null) + return; + + AddInternal(sample = new DrawableSample(ch) + { + Looping = Looping, + Volume = { Value = sampleInfo.Volume / 100.0 } + }); + } + + public void Play(bool restart = true) => sample?.Play(restart); + + public void Stop() => sample?.Stop(); + + public bool Playing => sample?.Playing ?? false; + + private bool looping; + + public bool Looping + { + get => looping; + set + { + looping = value; + + if (sample != null) + sample.Looping = value; + } + } + + /// + /// The volume of this component. + /// + public BindableNumber Volume => adjustments.Volume; + + /// + /// The playback balance of this sample (-1 .. 1 where 0 is centered) + /// + public BindableNumber Balance => adjustments.Balance; + + /// + /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. + /// + public BindableNumber Frequency => adjustments.Frequency; + + /// + /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. + /// + public BindableNumber Tempo => adjustments.Tempo; + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => adjustments.AddAdjustment(type, adjustBindable); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => adjustments.RemoveAdjustment(type, adjustBindable); + + public void RemoveAllAdjustments(AdjustableProperty type) => adjustments.RemoveAllAdjustments(type); + + public IBindable AggregateVolume => adjustments.AggregateVolume; + + public IBindable AggregateBalance => adjustments.AggregateBalance; + + public IBindable AggregateFrequency => adjustments.AggregateFrequency; + + public IBindable AggregateTempo => adjustments.AggregateTempo; + } +} diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 8410b7eeae..77ae8f1e16 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -4,144 +4,17 @@ using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Game.Audio; namespace osu.Game.Skinning { - public interface IPooledSampleProvider - { - [CanBeNull] - PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); - } - - public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent - { - private ISampleInfo sampleInfo; - private DrawableSample sample; - - [Resolved] - private ISampleStore sampleStore { get; set; } - - [Cached] - private readonly AudioAdjustments adjustments = new AudioAdjustments(); - - public PoolableSkinnableSample() - { - } - - public PoolableSkinnableSample(ISampleInfo sampleInfo) - { - Apply(sampleInfo); - } - - public void Apply(ISampleInfo sampleInfo) - { - if (this.sampleInfo != null) - throw new InvalidOperationException($"A {nameof(PoolableSkinnableSample)} cannot be applied multiple {nameof(ISampleInfo)}s."); - - this.sampleInfo = sampleInfo; - - if (LoadState >= LoadState.Ready) - updateSample(); - } - - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - base.SkinChanged(skin, allowFallback); - updateSample(); - } - - private void updateSample() - { - ClearInternal(); - - var ch = CurrentSkin.GetSample(sampleInfo); - - if (ch == null && AllowDefaultFallback) - { - foreach (var lookup in sampleInfo.LookupNames) - { - if ((ch = sampleStore.Get(lookup)) != null) - break; - } - } - - if (ch == null) - return; - - AddInternal(sample = new DrawableSample(ch) - { - Looping = Looping, - Volume = { Value = sampleInfo.Volume / 100.0 } - }); - } - - public void Play(bool restart = true) => sample?.Play(restart); - - public void Stop() => sample?.Stop(); - - public bool Playing => sample?.Playing ?? false; - - private bool looping; - - public bool Looping - { - get => looping; - set - { - looping = value; - - if (sample != null) - sample.Looping = value; - } - } - - /// - /// The volume of this component. - /// - public BindableNumber Volume => adjustments.Volume; - - /// - /// The playback balance of this sample (-1 .. 1 where 0 is centered) - /// - public BindableNumber Balance => adjustments.Balance; - - /// - /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. - /// - public BindableNumber Frequency => adjustments.Frequency; - - /// - /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. - /// - public BindableNumber Tempo => adjustments.Tempo; - - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => adjustments.AddAdjustment(type, adjustBindable); - - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => adjustments.RemoveAdjustment(type, adjustBindable); - - public void RemoveAllAdjustments(AdjustableProperty type) => adjustments.RemoveAllAdjustments(type); - - public IBindable AggregateVolume => adjustments.AggregateVolume; - - public IBindable AggregateBalance => adjustments.AggregateBalance; - - public IBindable AggregateFrequency => adjustments.AggregateFrequency; - - public IBindable AggregateTempo => adjustments.AggregateTempo; - } - public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent { public override bool RemoveWhenNotAlive => false; From 8920534a255fe7c182a33d3200a8c23dd177e299 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 20:24:03 +0900 Subject: [PATCH 03/48] Fix pools not being added to hierarchy --- osu.Game/Rulesets/UI/Playfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 9f3a4c508f..5383c4b2ce 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -348,7 +348,7 @@ namespace osu.Game.Rulesets.UI public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) { if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) - samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 5); + AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 5)); return existingPool.Get(); } From 812d5d59b1ac12757a86cb44bd6447b9f7d60846 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 20:29:09 +0900 Subject: [PATCH 04/48] Fix looping not being propagated --- osu.Game/Skinning/SkinnableSound.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 77ae8f1e16..eca1b9b03f 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -115,7 +115,12 @@ namespace osu.Game.Skinning SamplesContainer.Clear(); foreach (var s in samples) - SamplesContainer.Add(pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s)); + { + var sample = pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); + sample.Looping = Looping; + + SamplesContainer.Add(sample); + } if (wasPlaying) Play(); From 70cb1979578e51fda79a41b12f2767777d64cf08 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 20:38:36 +0900 Subject: [PATCH 05/48] Cleanups --- .../Objects/Drawables/DrawableHitObject.cs | 7 +++---- osu.Game/Skinning/PausableSkinnableSound.cs | 11 ++++++++--- osu.Game/Skinning/SkinnableSound.cs | 15 ++++++++++----- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 0d97066b35..26d2ffe3ce 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Objects.Drawables config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); // Explicit non-virtual function call. - base.AddInternal(Samples = new PausableSkinnableSound(Array.Empty())); + base.AddInternal(Samples = new PausableSkinnableSound()); } protected override void LoadAsyncComplete() @@ -266,7 +266,8 @@ namespace osu.Game.Rulesets.Objects.Drawables // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). samplesBindable.CollectionChanged -= onSamplesChanged; - Samples.Samples = Array.Empty(); + // Release the samples for other hitobjects to use. + Samples.Samples = null; if (nestedHitObjects.IsValueCreated) { @@ -334,8 +335,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual void LoadSamples() { - Samples.Samples = Array.Empty(); - var samples = GetSamples().ToArray(); if (samples.Length <= 0) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 758b784649..be4664356d 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Threading; @@ -14,16 +15,20 @@ namespace osu.Game.Skinning { protected bool RequestedPlaying { get; private set; } - public PausableSkinnableSound(ISampleInfo sample) - : base(sample) + public PausableSkinnableSound() { } - public PausableSkinnableSound(IEnumerable samples) + public PausableSkinnableSound([NotNull] IEnumerable samples) : base(samples) { } + public PausableSkinnableSound([NotNull] ISampleInfo sample) + : base(sample) + { + } + private readonly IBindable samplePlaybackDisabled = new Bindable(); private ScheduledDelegate scheduledStart; diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index eca1b9b03f..67d13118d9 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -38,18 +39,23 @@ namespace osu.Game.Skinning [Resolved(CanBeNull = true)] private IPooledSampleProvider pooledProvider { get; set; } - public SkinnableSound(ISampleInfo sample) - : this(new[] { sample }) + public SkinnableSound() { } - public SkinnableSound(IEnumerable samples) + public SkinnableSound([NotNull] IEnumerable samples) + : this() { this.samples = samples.ToArray(); InternalChild = SamplesContainer = new AudioContainer(); } + public SkinnableSound([NotNull] ISampleInfo sample) + : this(new[] { sample }) + { + } + private ISampleInfo[] samples; public ISampleInfo[] Samples @@ -57,8 +63,7 @@ namespace osu.Game.Skinning get => samples; set { - if (value == null) - throw new ArgumentNullException(nameof(value)); + value ??= Array.Empty(); if (samples == value) return; From f013928fa3dd7c0293cef09c8c2b651ea2e8e314 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 20:40:01 +0900 Subject: [PATCH 06/48] Set maximum pool size --- osu.Game/Rulesets/UI/Playfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 5383c4b2ce..6e89f20246 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -348,7 +348,7 @@ namespace osu.Game.Rulesets.UI public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) { if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) - AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 5)); + AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1, 30)); return existingPool.Get(); } From 688a04c2ff8e6a931b67ed4a36337508f5555d22 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 20:40:30 +0900 Subject: [PATCH 07/48] Make slider/spinner use pooled samples --- .../Objects/Drawables/DrawableSlider.cs | 17 ++++------- .../Objects/Drawables/DrawableSpinner.cs | 29 +++++++++++-------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 14c494d909..b62c04eed9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Game.Audio; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container tailContainer; private Container tickContainer; private Container repeatContainer; - private Container samplesContainer; + private PausableSkinnableSound slidingSample; public DrawableSlider() : this(null) @@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Alpha = 0 }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, - samplesContainer = new Container { RelativeSizeAxes = Axes.Both } + slidingSample = new PausableSkinnableSound { Looping = true } }; PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); @@ -100,17 +101,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnFree(hitObject); PathVersion.UnbindFrom(HitObject.Path.Version); - } - private PausableSkinnableSound slidingSample; + slidingSample.Samples = null; + } protected override void LoadSamples() { base.LoadSamples(); - samplesContainer.Clear(); - slidingSample = null; - var firstSample = HitObject.Samples.FirstOrDefault(); if (firstSample != null) @@ -118,10 +116,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "sliderslide"; - samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone) - { - Looping = true - }); + slidingSample.Samples = new ISampleInfo[] { clone }; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 2a14a7c975..e5fc717504 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -9,6 +9,7 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container ticks; private SpinnerBonusDisplay bonusDisplay; - private Container samplesContainer; + private PausableSkinnableSound spinningSample; private Bindable isSpinning; private bool spinnerFrequencyModulate; @@ -81,7 +82,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre, Y = -120, }, - samplesContainer = new Container { RelativeSizeAxes = Axes.Both } + spinningSample = new PausableSkinnableSound + { + Volume = { Value = 0 }, + Looping = true, + Frequency = { Value = spinning_sample_initial_frequency } + } }; PositionBindable.BindValueChanged(pos => Position = pos.NewValue); @@ -95,17 +101,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables isSpinning.BindValueChanged(updateSpinningSample); } - private PausableSkinnableSound spinningSample; private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; + protected override void OnFree(HitObject hitObject) + { + base.OnFree(hitObject); + + spinningSample.Samples = null; + } + protected override void LoadSamples() { base.LoadSamples(); - samplesContainer.Clear(); - spinningSample = null; - var firstSample = HitObject.Samples.FirstOrDefault(); if (firstSample != null) @@ -113,12 +122,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "spinnerspin"; - samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone) - { - Volume = { Value = 0 }, - Looping = true, - Frequency = { Value = spinning_sample_initial_frequency } - }); + spinningSample.Samples = new ISampleInfo[] { clone }; + spinningSample.Frequency.Value = spinning_sample_initial_frequency; } } From 0287269b2fed663d910f9571042accfe3a11d185 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 21:01:38 +0900 Subject: [PATCH 08/48] Fix volume discrepancies --- osu.Game/Skinning/PoolableSkinnableSample.cs | 42 ++++++++++---------- osu.Game/Skinning/SkinnableSound.cs | 4 +- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 6ad7345954..ad799dd32e 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -8,26 +8,28 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; using osu.Game.Audio; namespace osu.Game.Skinning { public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent { + private readonly AudioContainer sampleContainer; + private ISampleInfo sampleInfo; private DrawableSample sample; [Resolved] private ISampleStore sampleStore { get; set; } - [Cached] - private readonly AudioAdjustments adjustments = new AudioAdjustments(); - public PoolableSkinnableSample() { + InternalChild = sampleContainer = new AudioContainer { RelativeSizeAxes = Axes.Both }; } public PoolableSkinnableSample(ISampleInfo sampleInfo) + : this() { Apply(sampleInfo); } @@ -39,6 +41,8 @@ namespace osu.Game.Skinning this.sampleInfo = sampleInfo; + Volume.Value = sampleInfo.Volume / 100.0; + if (LoadState >= LoadState.Ready) updateSample(); } @@ -51,7 +55,7 @@ namespace osu.Game.Skinning private void updateSample() { - ClearInternal(); + sampleContainer.Clear(); var ch = CurrentSkin.GetSample(sampleInfo); @@ -67,11 +71,7 @@ namespace osu.Game.Skinning if (ch == null) return; - AddInternal(sample = new DrawableSample(ch) - { - Looping = Looping, - Volume = { Value = sampleInfo.Volume / 100.0 } - }); + sampleContainer.Add(sample = new DrawableSample(ch) { Looping = Looping }); } public void Play(bool restart = true) => sample?.Play(restart); @@ -97,37 +97,35 @@ namespace osu.Game.Skinning /// /// The volume of this component. /// - public BindableNumber Volume => adjustments.Volume; + public BindableNumber Volume => sampleContainer.Volume; /// /// The playback balance of this sample (-1 .. 1 where 0 is centered) /// - public BindableNumber Balance => adjustments.Balance; + public BindableNumber Balance => sampleContainer.Balance; /// /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. /// - public BindableNumber Frequency => adjustments.Frequency; + public BindableNumber Frequency => sampleContainer.Frequency; /// /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. /// - public BindableNumber Tempo => adjustments.Tempo; + public BindableNumber Tempo => sampleContainer.Tempo; - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => adjustments.AddAdjustment(type, adjustBindable); + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => adjustments.RemoveAdjustment(type, adjustBindable); + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable); - public void RemoveAllAdjustments(AdjustableProperty type) => adjustments.RemoveAllAdjustments(type); + public void RemoveAllAdjustments(AdjustableProperty type) => sampleContainer.RemoveAllAdjustments(type); - public IBindable AggregateVolume => adjustments.AggregateVolume; + public IBindable AggregateVolume => sampleContainer.AggregateVolume; - public IBindable AggregateBalance => adjustments.AggregateBalance; + public IBindable AggregateBalance => sampleContainer.AggregateBalance; - public IBindable AggregateFrequency => adjustments.AggregateFrequency; + public IBindable AggregateFrequency => sampleContainer.AggregateFrequency; - public IBindable AggregateTempo => adjustments.AggregateTempo; + public IBindable AggregateTempo => sampleContainer.AggregateTempo; } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 67d13118d9..8dc3337525 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -41,14 +41,13 @@ namespace osu.Game.Skinning public SkinnableSound() { + InternalChild = SamplesContainer = new AudioContainer(); } public SkinnableSound([NotNull] IEnumerable samples) : this() { this.samples = samples.ToArray(); - - InternalChild = SamplesContainer = new AudioContainer(); } public SkinnableSound([NotNull] ISampleInfo sample) @@ -123,6 +122,7 @@ namespace osu.Game.Skinning { var sample = pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); sample.Looping = Looping; + sample.Volume.Value = s.Volume / 100.0; SamplesContainer.Add(sample); } From 7180bfe4ba904507fd944d42bad39296a559dabc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 21:21:57 +0900 Subject: [PATCH 09/48] Unlimit number of samples per pool --- osu.Game/Rulesets/UI/Playfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 6e89f20246..460251e595 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -348,7 +348,7 @@ namespace osu.Game.Rulesets.UI public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) { if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) - AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1, 30)); + AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1)); return existingPool.Get(); } From d388c44428c74f2ce32cb48b0a83f900497f46b7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 22:30:21 +0900 Subject: [PATCH 10/48] Cleanup, refactoring, and restart sample on skin change --- osu.Game/Skinning/PoolableSkinnableSample.cs | 72 ++++++++++++++------ osu.Game/Skinning/SkinnableSound.cs | 7 -- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index ad799dd32e..e38b309efb 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -13,27 +14,48 @@ using osu.Game.Audio; namespace osu.Game.Skinning { + /// + /// A sample corresponding to an that supports being pooled and responding to skin changes. + /// public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent { - private readonly AudioContainer sampleContainer; + /// + /// The currently-loaded . + /// + [CanBeNull] + public DrawableSample Sample { get; private set; } + private readonly AudioContainer sampleContainer; private ISampleInfo sampleInfo; - private DrawableSample sample; [Resolved] private ISampleStore sampleStore { get; set; } + /// + /// Creates a new with no applied . + /// An can be applied later via . + /// public PoolableSkinnableSample() { InternalChild = sampleContainer = new AudioContainer { RelativeSizeAxes = Axes.Both }; } + /// + /// Creates a new with an applied . + /// + /// The to attach. public PoolableSkinnableSample(ISampleInfo sampleInfo) : this() { Apply(sampleInfo); } + /// + /// Applies an that describes the sample to retrieve. + /// Only one can ever be applied to a . + /// + /// The to apply. + /// If an has already been applied to this . public void Apply(ISampleInfo sampleInfo) { if (this.sampleInfo != null) @@ -55,6 +77,11 @@ namespace osu.Game.Skinning private void updateSample() { + if (sampleInfo == null) + return; + + bool wasPlaying = Playing; + sampleContainer.Clear(); var ch = CurrentSkin.GetSample(sampleInfo); @@ -71,17 +98,34 @@ namespace osu.Game.Skinning if (ch == null) return; - sampleContainer.Add(sample = new DrawableSample(ch) { Looping = Looping }); + sampleContainer.Add(Sample = new DrawableSample(ch) { Looping = Looping }); + + // Start playback internally for the new sample if the previous one was playing beforehand. + if (wasPlaying) + Play(); } - public void Play(bool restart = true) => sample?.Play(restart); + /// + /// Plays the sample. + /// + /// Whether to play the sample from the beginning. + public void Play(bool restart = true) => Sample?.Play(restart); - public void Stop() => sample?.Stop(); + /// + /// Stops the sample. + /// + public void Stop() => Sample?.Stop(); - public bool Playing => sample?.Playing ?? false; + /// + /// Whether the sample is currently playing. + /// + public bool Playing => Sample?.Playing ?? false; private bool looping; + /// + /// Gets or sets whether the sample should loop on completion. + /// public bool Looping { get => looping; @@ -89,29 +133,17 @@ namespace osu.Game.Skinning { looping = value; - if (sample != null) - sample.Looping = value; + if (Sample != null) + Sample.Looping = value; } } - /// - /// The volume of this component. - /// public BindableNumber Volume => sampleContainer.Volume; - /// - /// The playback balance of this sample (-1 .. 1 where 0 is centered) - /// public BindableNumber Balance => sampleContainer.Balance; - /// - /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. - /// public BindableNumber Frequency => sampleContainer.Frequency; - /// - /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. - /// public BindableNumber Tempo => sampleContainer.Tempo; public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 8dc3337525..0f39784138 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -103,13 +103,6 @@ namespace osu.Game.Skinning SamplesContainer.ForEach(c => c.Stop()); } - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - // Start playback internally for the new samples if the previous ones were playing beforehand. - if (IsPlaying) - Play(); - } - private void updateSamples() { bool wasPlaying = IsPlaying; From a53848ef9bbc09b8c4c75a044c3db6a543f85dc0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 22:30:41 +0900 Subject: [PATCH 11/48] Fix storyboard imlpementation --- osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index b8d212d3c9..904af730de 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -37,8 +37,8 @@ namespace osu.Game.Storyboards.Drawables foreach (var mod in mods.Value.OfType()) { - // foreach (var sample in SamplesContainer) - // mod.ApplyToSample(sample.Sample); + foreach (var sample in SamplesContainer) + mod.ApplyToSample(sample.Sample); } } From 8a656f7cee1ac51578420e9e9576c8055a8fde67 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 22:42:44 +0900 Subject: [PATCH 12/48] Fix missing SkinChanged event + safety --- osu.Game/Skinning/SkinnableSound.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 0f39784138..bb747a2176 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -55,7 +55,7 @@ namespace osu.Game.Skinning { } - private ISampleInfo[] samples; + private ISampleInfo[] samples = Array.Empty(); public ISampleInfo[] Samples { @@ -103,6 +103,12 @@ namespace osu.Game.Skinning SamplesContainer.ForEach(c => c.Stop()); } + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateSamples(); + } + private void updateSamples() { bool wasPlaying = IsPlaying; From 7c83a27002a033a114b42a6b933c998d8858f5fa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 22:47:11 +0900 Subject: [PATCH 13/48] Add more xmldocs --- osu.Game/Skinning/PoolableSkinnableSample.cs | 2 +- osu.Game/Skinning/SkinnableSound.cs | 29 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index e38b309efb..adc58ee94e 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -124,7 +124,7 @@ namespace osu.Game.Skinning private bool looping; /// - /// Gets or sets whether the sample should loop on completion. + /// Whether the sample should loop on completion. /// public bool Looping { diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index bb747a2176..24dddaf758 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -16,6 +16,9 @@ using osu.Game.Audio; namespace osu.Game.Skinning { + /// + /// A sound consisting of one or more samples to be played. + /// public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent { public override bool RemoveWhenNotAlive => false; @@ -39,17 +42,28 @@ namespace osu.Game.Skinning [Resolved(CanBeNull = true)] private IPooledSampleProvider pooledProvider { get; set; } + /// + /// Creates a new . + /// public SkinnableSound() { InternalChild = SamplesContainer = new AudioContainer(); } + /// + /// Creates a new with some initial samples. + /// + /// The initial samples. public SkinnableSound([NotNull] IEnumerable samples) : this() { this.samples = samples.ToArray(); } + /// + /// Creates a new with an initial sample. + /// + /// The initial sample. public SkinnableSound([NotNull] ISampleInfo sample) : this(new[] { sample }) { @@ -57,6 +71,9 @@ namespace osu.Game.Skinning private ISampleInfo[] samples = Array.Empty(); + /// + /// The samples that should be played. + /// public ISampleInfo[] Samples { get => samples; @@ -76,6 +93,9 @@ namespace osu.Game.Skinning private bool looping; + /// + /// Whether the samples should loop on completion. + /// public bool Looping { get => looping; @@ -89,6 +109,9 @@ namespace osu.Game.Skinning } } + /// + /// Plays the samples. + /// public virtual void Play() { SamplesContainer.ForEach(c => @@ -98,6 +121,9 @@ namespace osu.Game.Skinning }); } + /// + /// Stops the samples. + /// public virtual void Stop() { SamplesContainer.ForEach(c => c.Stop()); @@ -149,6 +175,9 @@ namespace osu.Game.Skinning public void RemoveAllAdjustments(AdjustableProperty type) => SamplesContainer.RemoveAllAdjustments(type); + /// + /// Whether any samples currently playing. + /// public bool IsPlaying => SamplesContainer.Any(s => s.Playing); #endregion From afb8eb636ddc4cd589755f796f1b8bad9e5adb30 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Nov 2020 18:40:22 +0900 Subject: [PATCH 14/48] Apply simple PR reviews --- osu.Game/Skinning/IPooledSampleProvider.cs | 10 +++++++++- osu.Game/Skinning/PoolableSkinnableSample.cs | 4 ++++ osu.Game/Skinning/SkinnableSound.cs | 6 +++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs index 3dc0b5375d..5dbbadcc8a 100644 --- a/osu.Game/Skinning/IPooledSampleProvider.cs +++ b/osu.Game/Skinning/IPooledSampleProvider.cs @@ -6,8 +6,16 @@ using osu.Game.Audio; namespace osu.Game.Skinning { - public interface IPooledSampleProvider + /// + /// Provides pooled samples to be used by s. + /// + internal interface IPooledSampleProvider { + /// + /// Retrieves a from a pool. + /// + /// The describing the sample to retrieve.. + /// The . [CanBeNull] PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); } diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index adc58ee94e..7e885b20d1 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -138,6 +138,8 @@ namespace osu.Game.Skinning } } + #region Re-expose AudioContainer + public BindableNumber Volume => sampleContainer.Volume; public BindableNumber Balance => sampleContainer.Balance; @@ -159,5 +161,7 @@ namespace osu.Game.Skinning public IBindable AggregateFrequency => sampleContainer.AggregateFrequency; public IBindable AggregateTempo => sampleContainer.AggregateTempo; + + #endregion } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 24dddaf758..46c2e4b749 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -40,7 +40,7 @@ namespace osu.Game.Skinning private ISampleStore sampleStore { get; set; } [Resolved(CanBeNull = true)] - private IPooledSampleProvider pooledProvider { get; set; } + private IPooledSampleProvider samplePool { get; set; } /// /// Creates a new . @@ -145,7 +145,7 @@ namespace osu.Game.Skinning foreach (var s in samples) { - var sample = pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); + var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); sample.Looping = Looping; sample.Volume.Value = s.Volume / 100.0; @@ -176,7 +176,7 @@ namespace osu.Game.Skinning => SamplesContainer.RemoveAllAdjustments(type); /// - /// Whether any samples currently playing. + /// Whether any samples are currently playing. /// public bool IsPlaying => SamplesContainer.Any(s => s.Playing); From f5e8d1d14d47934976b930df6cca83831ce07a6b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Nov 2020 19:19:24 +0900 Subject: [PATCH 15/48] Lose old reference immediately on updateSample() --- osu.Game/Skinning/PoolableSkinnableSample.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 7e885b20d1..19b96d6c60 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -83,6 +83,7 @@ namespace osu.Game.Skinning bool wasPlaying = Playing; sampleContainer.Clear(); + Sample = null; var ch = CurrentSkin.GetSample(sampleInfo); From f589da43177a1df974bbfc8e59e4596459c22672 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Nov 2020 19:24:38 +0900 Subject: [PATCH 16/48] Fix compilation error --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index e5fc717504..1ba17d9e17 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -104,9 +104,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; - protected override void OnFree(HitObject hitObject) + protected override void OnFree() { - base.OnFree(hitObject); + base.OnFree(); spinningSample.Samples = null; } From c29ad8edf8731bbaf0d3189a2917b00b7f7cde33 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Nov 2020 19:26:25 +0900 Subject: [PATCH 17/48] Better API for dealing with the contained drawable samples --- osu.Game/Skinning/SkinnableSound.cs | 39 +++++++++++-------- .../Drawables/DrawableStoryboardSample.cs | 4 +- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 46c2e4b749..23159e4fe1 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -11,6 +11,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Game.Audio; @@ -34,7 +35,13 @@ namespace osu.Game.Skinning /// protected bool PlayWhenZeroVolume => Looping; - protected readonly AudioContainer SamplesContainer; + /// + /// All raw s contained in this . + /// + [NotNull, ItemNotNull] + protected IEnumerable DrawableSamples => samplesContainer.Select(c => c.Sample).Where(s => s != null); + + private readonly AudioContainer samplesContainer; [Resolved] private ISampleStore sampleStore { get; set; } @@ -47,7 +54,7 @@ namespace osu.Game.Skinning /// public SkinnableSound() { - InternalChild = SamplesContainer = new AudioContainer(); + InternalChild = samplesContainer = new AudioContainer(); } /// @@ -105,7 +112,7 @@ namespace osu.Game.Skinning looping = value; - SamplesContainer.ForEach(c => c.Looping = looping); + samplesContainer.ForEach(c => c.Looping = looping); } } @@ -114,7 +121,7 @@ namespace osu.Game.Skinning /// public virtual void Play() { - SamplesContainer.ForEach(c => + samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) c.Play(); @@ -126,7 +133,7 @@ namespace osu.Game.Skinning /// public virtual void Stop() { - SamplesContainer.ForEach(c => c.Stop()); + samplesContainer.ForEach(c => c.Stop()); } protected override void SkinChanged(ISkinSource skin, bool allowFallback) @@ -140,8 +147,8 @@ namespace osu.Game.Skinning bool wasPlaying = IsPlaying; // Remove all pooled samples (return them to the pool), and dispose the rest. - SamplesContainer.RemoveAll(s => s.IsInPool); - SamplesContainer.Clear(); + samplesContainer.RemoveAll(s => s.IsInPool); + samplesContainer.Clear(); foreach (var s in samples) { @@ -149,7 +156,7 @@ namespace osu.Game.Skinning sample.Looping = Looping; sample.Volume.Value = s.Volume / 100.0; - SamplesContainer.Add(sample); + samplesContainer.Add(sample); } if (wasPlaying) @@ -158,27 +165,27 @@ namespace osu.Game.Skinning #region Re-expose AudioContainer - public BindableNumber Volume => SamplesContainer.Volume; + public BindableNumber Volume => samplesContainer.Volume; - public BindableNumber Balance => SamplesContainer.Balance; + public BindableNumber Balance => samplesContainer.Balance; - public BindableNumber Frequency => SamplesContainer.Frequency; + public BindableNumber Frequency => samplesContainer.Frequency; - public BindableNumber Tempo => SamplesContainer.Tempo; + public BindableNumber Tempo => samplesContainer.Tempo; public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => SamplesContainer.AddAdjustment(type, adjustBindable); + => samplesContainer.AddAdjustment(type, adjustBindable); public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => SamplesContainer.RemoveAdjustment(type, adjustBindable); + => samplesContainer.RemoveAdjustment(type, adjustBindable); public void RemoveAllAdjustments(AdjustableProperty type) - => SamplesContainer.RemoveAllAdjustments(type); + => samplesContainer.RemoveAllAdjustments(type); /// /// Whether any samples are currently playing. /// - public bool IsPlaying => SamplesContainer.Any(s => s.Playing); + public bool IsPlaying => samplesContainer.Any(s => s.Playing); #endregion } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 904af730de..218f051bf0 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -37,8 +37,8 @@ namespace osu.Game.Storyboards.Drawables foreach (var mod in mods.Value.OfType()) { - foreach (var sample in SamplesContainer) - mod.ApplyToSample(sample.Sample); + foreach (var sample in DrawableSamples) + mod.ApplyToSample(sample); } } From 2c57deea2bd2724a0a333146f58c2002c18e0c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Nov 2020 19:43:20 +0100 Subject: [PATCH 18/48] Trim double full-stop in xmldoc --- osu.Game/Skinning/IPooledSampleProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs index 5dbbadcc8a..40193d1a1a 100644 --- a/osu.Game/Skinning/IPooledSampleProvider.cs +++ b/osu.Game/Skinning/IPooledSampleProvider.cs @@ -14,7 +14,7 @@ namespace osu.Game.Skinning /// /// Retrieves a from a pool. /// - /// The describing the sample to retrieve.. + /// The describing the sample to retrieve. /// The . [CanBeNull] PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); From b780fdbe4c24efce455c6ebc1b0d53dcc3826eb3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 18:08:59 +0900 Subject: [PATCH 19/48] Ignore lookup types for JSON serialisation --- osu.Game/Audio/HitSampleInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 4a0e48749a..27fb5d2f8e 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -56,6 +56,7 @@ namespace osu.Game.Audio /// /// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first). /// + [JsonIgnore] public virtual IEnumerable LookupNames { get From dda4d76d726592aa3fa7c23639611b3e6694b56d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 18:09:21 +0900 Subject: [PATCH 20/48] Fix bad equality comparer implementations --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 2 +- osu.Game/Audio/HitSampleInfo.cs | 2 +- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index ec4b67f341..7734ebed12 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Objects => other != null; public override bool Equals(object obj) - => Equals((BananaHitSampleInfo)obj); + => obj is BananaHitSampleInfo other && Equals(other); public override int GetHashCode() => lookup_names.GetHashCode(); } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 27fb5d2f8e..9bd34c009b 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -83,7 +83,7 @@ namespace osu.Game.Audio => other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix; public override bool Equals(object? obj) - => Equals((HitSampleInfo?)obj); + => obj is HitSampleInfo other && Equals(other); public override int GetHashCode() => HashCode.Combine(Name, Bank, Suffix); } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 64694fb12e..015d6a3dd6 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -496,7 +496,8 @@ namespace osu.Game.Rulesets.Objects.Legacy public bool Equals(LegacyHitSampleInfo? other) => base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered; - public override bool Equals(object? obj) => Equals((LegacyHitSampleInfo?)obj); + public override bool Equals(object? obj) + => obj is LegacyHitSampleInfo other && Equals(other); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered); } @@ -529,7 +530,7 @@ namespace osu.Game.Rulesets.Objects.Legacy => base.Equals(other) && Filename == other.Filename; public override bool Equals(object? obj) - => Equals((FileHitSampleInfo?)obj); + => obj is FileHitSampleInfo other && Equals(other); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Filename); } From 17560aeeea75adb03347d31e02a85f2b930b0954 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 18:09:28 +0900 Subject: [PATCH 21/48] Volume should be 0 by default --- osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs | 2 +- osu.Game/Audio/HitSampleInfo.cs | 3 ++- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 0d57fb7029..f552c3c27b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests NewCombo = i % 8 == 0, Samples = new List(new[] { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal") + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 100) }) }); } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 9bd34c009b..58b078db71 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json; using osu.Game.Utils; namespace osu.Game.Audio @@ -45,7 +46,7 @@ namespace osu.Game.Audio /// public int Volume { get; } - public HitSampleInfo(string name, string? bank = null, string? suffix = null, int volume = 100) + public HitSampleInfo(string name, string? bank = null, string? suffix = null, int volume = 0) { Name = name; Bank = bank; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 015d6a3dd6..762edf5a13 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -479,7 +479,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// public readonly bool IsLayered; - public LegacyHitSampleInfo(string name, string? bank = null, int volume = 100, int customSampleBank = 0, bool isLayered = false) + public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, int customSampleBank = 0, bool isLayered = false) : base(name, bank, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume) { CustomSampleBank = customSampleBank; From 598572195c0f04acdc5c6785a88668464107f108 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 19:57:25 +0900 Subject: [PATCH 22/48] Add playlist length to match settings overlay --- .../Match/Components/MatchSettingsOverlay.cs | 21 +++++++++++++++++++ osu.Game/Tests/Beatmaps/TestBeatmap.cs | 1 + 2 files changed, 22 insertions(+) diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index caefc194b1..8bf66b084c 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Specialized; +using System.Linq; using Humanizer; +using Humanizer.Localisation; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -69,6 +72,7 @@ namespace osu.Game.Screens.Multi.Match.Components private OsuSpriteText typeLabel; private LoadingLayer loadingLayer; private DrawableRoomPlaylist playlist; + private OsuSpriteText playlistLength; [Resolved(CanBeNull = true)] private IRoomManager manager { get; set; } @@ -229,6 +233,15 @@ namespace osu.Game.Screens.Multi.Match.Components playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both } }, new Drawable[] + { + playlistLength = new OsuSpriteText + { + Margin = new MarginPadding { Vertical = 5 }, + Colour = colours.Yellow, + Font = OsuFont.GetFont(size: 12), + } + }, + new Drawable[] { new PurpleTriangleButton { @@ -243,6 +256,7 @@ namespace osu.Game.Screens.Multi.Match.Components { new Dimension(), new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), } } }, @@ -315,6 +329,7 @@ namespace osu.Game.Screens.Multi.Match.Components Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue, true); playlist.Items.BindTo(Playlist); + Playlist.BindCollectionChanged(onPlaylistChanged, true); } protected override void Update() @@ -324,6 +339,12 @@ namespace osu.Game.Screens.Multi.Match.Components ApplyButton.Enabled.Value = hasValidSettings; } + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) + { + double totalLength = Playlist.Select(p => p.Beatmap.Value.Length).Sum(); + playlistLength.Text = $"Length: {totalLength.Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2)}"; + } + private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; private void apply() diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 87b77f4616..035cb64099 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -31,6 +31,7 @@ namespace osu.Game.Tests.Beatmaps BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; BeatmapInfo.BeatmapSet.Files = new List(); BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo }; + BeatmapInfo.Length = 75000; BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo { Status = BeatmapSetOnlineStatus.Ranked, From e7c0e9834f41f5f52fb3b331a02c4742c84bf342 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 16:53:01 +0900 Subject: [PATCH 23/48] Introduce RandomSeed in catch DHO --- .../Drawables/DrawableCatchHitObject.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 1faa6a5b0f..eb5f9451b1 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -3,10 +3,13 @@ using System; using JetBrains.Annotations; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Utils; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -20,12 +23,32 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; + /// + /// The seed value used for visual randomness such as fruit rotation. + /// By default, truncated to an integer is used. + /// + public Bindable RandomSeed = new Bindable(); + protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject) : base(hitObject) { Anchor = Anchor.BottomLeft; } + [BackgroundDependencyLoader] + private void load() + { + StartTimeBindable.BindValueChanged(change => + { + RandomSeed.Value = (int)change.NewValue; + }, true); + } + + /// + /// Get a random number in range [0,1) based on seed . + /// + public float RandomSingle(int series) => StatelessRNG.NextSingle(RandomSeed.Value, series); + protected override void OnApply() { base.OnApply(); From 8b6161a51c27fce87f51a171a2a6f4c2e402e212 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 16:54:18 +0900 Subject: [PATCH 24/48] Use deterministic randomness in catch hit object --- .../Objects/Drawables/DrawableBanana.cs | 20 ++++++++++++++----- .../Objects/Drawables/DrawableDroplet.cs | 3 +-- .../Objects/Drawables/DrawableFruit.cs | 8 +++++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index fb982bbdab..4e34dd2b90 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -3,7 +3,6 @@ using JetBrains.Annotations; using osu.Framework.Graphics; -using osu.Framework.Utils; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -21,6 +20,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { } + protected override void LoadComplete() + { + base.LoadComplete(); + + RandomSeed.BindValueChanged(_ => + { + UpdateComboColour(); + UpdateInitialTransforms(); + }); + } + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); @@ -28,14 +38,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables const float end_scale = 0.6f; const float random_scale_range = 1.6f; - ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RNG.NextSingle())) + ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3))) .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); - ScaleContainer.RotateTo(getRandomAngle()) + ScaleContainer.RotateTo(getRandomAngle(1)) .Then() - .RotateTo(getRandomAngle(), HitObject.TimePreempt); + .RotateTo(getRandomAngle(2), HitObject.TimePreempt); - float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1); + float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1); } public override void PlaySamples() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 06ecd44488..b8acea625b 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -4,7 +4,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.UpdateInitialTransforms(); // roughly matches osu-stable - float startRotation = RNG.NextSingle() * 20; + float startRotation = RandomSingle(1) * 20; double duration = HitObject.TimePreempt + 2000; ScaleContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 68cb649b66..010a3ee08c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -5,7 +5,6 @@ using System; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -30,8 +29,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; - IndexInBeatmap.BindValueChanged(change => { VisualRepresentation.Value = GetVisualRepresentation(change.NewValue); @@ -39,6 +36,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables VisualRepresentation.BindValueChanged(_ => updatePiece()); HyperDash.BindValueChanged(_ => updatePiece(), true); + + RandomSeed.BindValueChanged(_ => + { + ScaleContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; + }, true); } private void updatePiece() From 8a78c495f22416e533249ae4173bb2e81b6411c4 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 16:55:37 +0900 Subject: [PATCH 25/48] Refactor DHO testing logic to the "specimen" class --- .../TestSceneFruitObjects.cs | 34 +++++++++++++------ .../TestSceneFruitVisualChange.cs | 4 +-- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 160da75aa9..3a651605d3 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -3,11 +3,12 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osuTK; namespace osu.Game.Rulesets.Catch.Tests { @@ -37,39 +38,50 @@ namespace osu.Game.Rulesets.Catch.Tests } private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) => - SetProperties(new DrawableFruit(new Fruit + new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit { IndexInBeatmap = indexInBeatmap, HyperDashBindable = { Value = hyperdash } })); private Drawable createDrawableBanana() => - SetProperties(new DrawableBanana(new Banana())); + new TestDrawableCatchHitObjectSpecimen(new DrawableBanana(new Banana())); private Drawable createDrawableDroplet(bool hyperdash = false) => - SetProperties(new DrawableDroplet(new Droplet + new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet { HyperDashBindable = { Value = hyperdash } })); - private Drawable createDrawableTinyDroplet() => SetProperties(new DrawableTinyDroplet(new TinyDroplet())); + private Drawable createDrawableTinyDroplet() => new TestDrawableCatchHitObjectSpecimen(new DrawableTinyDroplet(new TinyDroplet())); + } - protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d) + public class TestDrawableCatchHitObjectSpecimen : CompositeDrawable + { + public readonly ManualClock ManualClock; + + public TestDrawableCatchHitObjectSpecimen(DrawableCatchHitObject d) { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + ManualClock = new ManualClock(); + Clock = new FramedClock(ManualClock); + var hitObject = d.HitObject; - hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 0 }); - hitObject.StartTime = 1000000000000; + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); hitObject.Scale = 1.5f; + hitObject.StartTime = 500; d.Anchor = Anchor.Centre; - d.RelativePositionAxes = Axes.None; - d.Position = Vector2.Zero; d.HitObjectApplied += _ => { d.LifetimeStart = double.NegativeInfinity; d.LifetimeEnd = double.PositiveInfinity; }; - return d; + + InternalChild = d; } } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs index 4448e828e7..125e0c674c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests protected override void LoadComplete() { - AddStep("fruit changes visual and hyper", () => SetContents(() => SetProperties(new DrawableFruit(new Fruit + AddStep("fruit changes visual and hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit { IndexInBeatmapBindable = { BindTarget = indexInBeatmap }, HyperDashBindable = { BindTarget = hyperDash }, })))); - AddStep("droplet changes hyper", () => SetContents(() => SetProperties(new DrawableDroplet(new Droplet + AddStep("droplet changes hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet { HyperDashBindable = { BindTarget = hyperDash }, })))); From beda6961e433b7df28f69b898c975035e8ce24ee Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 16:55:58 +0900 Subject: [PATCH 26/48] Add a test for fruit randomness --- .../TestSceneFruitRandomness.cs | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs new file mode 100644 index 0000000000..449cfac2db --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneFruitRandomness : OsuTestScene + { + [Test] + public void TestFruitRandomness() + { + Bindable randomSeed = new Bindable(); + + TestDrawableFruit drawableFruit; + TestDrawableBanana drawableBanana; + + Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit = new TestDrawableFruit(new Fruit()) + { + RandomSeed = { BindTarget = randomSeed } + }) { X = -200 }); + Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana = new TestDrawableBanana(new Banana()) + { + RandomSeed = { BindTarget = randomSeed } + })); + + float fruitRotation = 0; + float bananaRotation = 0; + float bananaScale = 0; + + AddStep("set random seed to 0", () => + { + drawableFruit.HitObject.StartTime = 500; + randomSeed.Value = 0; + + fruitRotation = drawableFruit.InnerRotation; + bananaRotation = drawableBanana.InnerRotation; + bananaScale = drawableBanana.InnerScale; + }); + + AddStep("change random seed", () => + { + randomSeed.Value = 10; + }); + + AddAssert("fruit rotation is changed", () => drawableFruit.InnerRotation != fruitRotation); + AddAssert("banana rotation is changed", () => drawableBanana.InnerRotation != bananaRotation); + AddAssert("banana scale is changed", () => drawableBanana.InnerScale != bananaScale); + + AddStep("reset random seed", () => + { + randomSeed.Value = 0; + }); + + AddAssert("rotation and scale restored", () => + drawableFruit.InnerRotation == fruitRotation && + drawableBanana.InnerRotation == bananaRotation && + drawableBanana.InnerScale == bananaScale); + + AddStep("change start time", () => + { + drawableFruit.HitObject.StartTime = 1000; + }); + + AddAssert("random seed is changed", () => randomSeed.Value == 1000); + + AddSliderStep("random seed", 0, 100, 0, x => randomSeed.Value = x); + } + + private class TestDrawableFruit : DrawableFruit + { + public float InnerRotation => ScaleContainer.Rotation; + + public TestDrawableFruit(Fruit h) + : base(h) + { + } + } + + private class TestDrawableBanana : DrawableBanana + { + public float InnerRotation => ScaleContainer.Rotation; + public float InnerScale => ScaleContainer.Scale.X; + + public TestDrawableBanana(Banana h) + : base(h) + { + } + } + } +} From 08848e49de025022e9ae8ec0338e067987a53b03 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 17:12:30 +0900 Subject: [PATCH 27/48] Set banana combo colour using random seed --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 13 ++++--------- .../Objects/CatchHitObject.cs | 10 ++++++++++ .../Objects/Drawables/DrawableCatchHitObject.cs | 17 ++--------------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 7734ebed12..9fd01b3717 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -3,11 +3,11 @@ using System; using System.Collections.Generic; -using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Utils; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects @@ -28,17 +28,12 @@ namespace osu.Game.Rulesets.Catch.Objects Samples = samples; } - private Color4? colour; - - Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) - { - // override any external colour changes with banananana - return colour ??= getBananaColour(); - } + // override any external colour changes with banananana + Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => getBananaColour(); private Color4 getBananaColour() { - switch (RNG.Next(0, 3)) + switch (StatelessRNG.NextInt(3, RandomSeed.Value)) { default: return new Color4(255, 240, 0, 255); diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index a74055bff9..b9c5e42777 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -97,6 +97,12 @@ namespace osu.Game.Rulesets.Catch.Objects set => ScaleBindable.Value = value; } + /// + /// The seed value used for visual randomness such as fruit rotation. + /// By default, truncated to an integer is used. + /// + public Bindable RandomSeed = new Bindable(); + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -111,6 +117,10 @@ namespace osu.Game.Rulesets.Catch.Objects protected CatchHitObject() { XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset); + StartTimeBindable.BindValueChanged(change => + { + RandomSeed.Value = (int)change.NewValue; + }, true); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index eb5f9451b1..510431ff69 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -3,11 +3,9 @@ using System; using JetBrains.Annotations; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Utils; @@ -23,10 +21,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; - /// - /// The seed value used for visual randomness such as fruit rotation. - /// By default, truncated to an integer is used. - /// public Bindable RandomSeed = new Bindable(); protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject) @@ -35,15 +29,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Anchor = Anchor.BottomLeft; } - [BackgroundDependencyLoader] - private void load() - { - StartTimeBindable.BindValueChanged(change => - { - RandomSeed.Value = (int)change.NewValue; - }, true); - } - /// /// Get a random number in range [0,1) based on seed . /// @@ -54,6 +39,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.OnApply(); XBindable.BindTo(HitObject.XBindable); + RandomSeed.BindTo(HitObject.RandomSeed); } protected override void OnFree() @@ -61,6 +47,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.OnFree(); XBindable.UnbindFrom(HitObject.XBindable); + RandomSeed.UnbindFrom(HitObject.RandomSeed); } public Func CheckPosition; From ef741a1471170e62edbd34277a17e9966fcf97cf Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 17:16:36 +0900 Subject: [PATCH 28/48] Test banana colour change based on random seed --- osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 449cfac2db..244e2f14f4 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Tests.Visual; +using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Tests { @@ -31,6 +32,7 @@ namespace osu.Game.Rulesets.Catch.Tests float fruitRotation = 0; float bananaRotation = 0; float bananaScale = 0; + Color4 bananaColour = new Color4(); AddStep("set random seed to 0", () => { @@ -40,16 +42,19 @@ namespace osu.Game.Rulesets.Catch.Tests fruitRotation = drawableFruit.InnerRotation; bananaRotation = drawableBanana.InnerRotation; bananaScale = drawableBanana.InnerScale; + bananaColour = drawableBanana.AccentColour.Value; }); AddStep("change random seed", () => { + // Use a seed value such that the banana colour is different (2/3 of the seed values are okay). randomSeed.Value = 10; }); AddAssert("fruit rotation is changed", () => drawableFruit.InnerRotation != fruitRotation); AddAssert("banana rotation is changed", () => drawableBanana.InnerRotation != bananaRotation); AddAssert("banana scale is changed", () => drawableBanana.InnerScale != bananaScale); + AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour); AddStep("reset random seed", () => { @@ -59,7 +64,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("rotation and scale restored", () => drawableFruit.InnerRotation == fruitRotation && drawableBanana.InnerRotation == bananaRotation && - drawableBanana.InnerScale == bananaScale); + drawableBanana.InnerScale == bananaScale && + drawableBanana.AccentColour.Value == bananaColour); AddStep("change start time", () => { From 1a6b8e022cba52b12289b2b541effaad186a360e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 17:20:52 +0900 Subject: [PATCH 29/48] Fix formatting --- .../TestSceneFruitRandomness.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 244e2f14f4..38efcd85b8 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -17,17 +17,14 @@ namespace osu.Game.Rulesets.Catch.Tests { Bindable randomSeed = new Bindable(); - TestDrawableFruit drawableFruit; - TestDrawableBanana drawableBanana; + var drawableFruit = new TestDrawableFruit(new Fruit()); + var drawableBanana = new TestDrawableBanana(new Banana()); - Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit = new TestDrawableFruit(new Fruit()) - { - RandomSeed = { BindTarget = randomSeed } - }) { X = -200 }); - Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana = new TestDrawableBanana(new Banana()) - { - RandomSeed = { BindTarget = randomSeed } - })); + drawableFruit.RandomSeed.BindTo(randomSeed); + drawableBanana.RandomSeed.BindTo(randomSeed); + + Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 }); + Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana)); float fruitRotation = 0; float bananaRotation = 0; From 5936a8ffb4a1ea87720b7436bfdddb31ce47d49d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 18:06:14 +0900 Subject: [PATCH 30/48] Fix drawables are added multiple times in a test scene --- .../TestSceneFruitRandomness.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 38efcd85b8..5f7447323f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -12,13 +12,14 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneFruitRandomness : OsuTestScene { - [Test] - public void TestFruitRandomness() - { - Bindable randomSeed = new Bindable(); + private readonly Bindable randomSeed = new Bindable(); + private readonly TestDrawableFruit drawableFruit; + private readonly TestDrawableBanana drawableBanana; - var drawableFruit = new TestDrawableFruit(new Fruit()); - var drawableBanana = new TestDrawableBanana(new Banana()); + public TestSceneFruitRandomness() + { + drawableFruit = new TestDrawableFruit(new Fruit()); + drawableBanana = new TestDrawableBanana(new Banana()); drawableFruit.RandomSeed.BindTo(randomSeed); drawableBanana.RandomSeed.BindTo(randomSeed); @@ -26,6 +27,12 @@ namespace osu.Game.Rulesets.Catch.Tests Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 }); Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana)); + AddSliderStep("random seed", 0, 100, 0, x => randomSeed.Value = x); + } + + [Test] + public void TestFruitRandomness() + { float fruitRotation = 0; float bananaRotation = 0; float bananaScale = 0; @@ -70,8 +77,6 @@ namespace osu.Game.Rulesets.Catch.Tests }); AddAssert("random seed is changed", () => randomSeed.Value == 1000); - - AddSliderStep("random seed", 0, 100, 0, x => randomSeed.Value = x); } private class TestDrawableFruit : DrawableFruit From fdcfa81e4627c129f7bbcfe32280b09556c96914 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 20:53:47 +0900 Subject: [PATCH 31/48] Make RandomSeed a property, not a bindable --- .../TestSceneFruitRandomness.cs | 35 ++++++++----------- osu.Game.Rulesets.Catch/Objects/Banana.cs | 2 +- .../Objects/CatchHitObject.cs | 8 ++--- .../Objects/Drawables/DrawableBanana.cs | 7 ++-- .../Drawables/DrawableCatchHitObject.cs | 6 ++-- .../Objects/Drawables/DrawableFruit.cs | 10 +++--- 6 files changed, 27 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 5f7447323f..2ffebb7de1 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Bindables; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Tests.Visual; @@ -12,7 +11,6 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneFruitRandomness : OsuTestScene { - private readonly Bindable randomSeed = new Bindable(); private readonly TestDrawableFruit drawableFruit; private readonly TestDrawableBanana drawableBanana; @@ -21,27 +19,30 @@ namespace osu.Game.Rulesets.Catch.Tests drawableFruit = new TestDrawableFruit(new Fruit()); drawableBanana = new TestDrawableBanana(new Banana()); - drawableFruit.RandomSeed.BindTo(randomSeed); - drawableBanana.RandomSeed.BindTo(randomSeed); - Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 }); Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana)); - AddSliderStep("random seed", 0, 100, 0, x => randomSeed.Value = x); + AddSliderStep("start time", 500, 600, 0, x => + { + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x; + }); } [Test] public void TestFruitRandomness() { + // Use values such that the banana colour changes (2/3 of the integers are okay) + const int initial_start_time = 500; + const int another_start_time = 501; + float fruitRotation = 0; float bananaRotation = 0; float bananaScale = 0; Color4 bananaColour = new Color4(); - AddStep("set random seed to 0", () => + AddStep("Initialize start time", () => { - drawableFruit.HitObject.StartTime = 500; - randomSeed.Value = 0; + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; fruitRotation = drawableFruit.InnerRotation; bananaRotation = drawableBanana.InnerRotation; @@ -49,10 +50,9 @@ namespace osu.Game.Rulesets.Catch.Tests bananaColour = drawableBanana.AccentColour.Value; }); - AddStep("change random seed", () => + AddStep("change start time", () => { - // Use a seed value such that the banana colour is different (2/3 of the seed values are okay). - randomSeed.Value = 10; + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; }); AddAssert("fruit rotation is changed", () => drawableFruit.InnerRotation != fruitRotation); @@ -60,9 +60,9 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("banana scale is changed", () => drawableBanana.InnerScale != bananaScale); AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour); - AddStep("reset random seed", () => + AddStep("reset start time", () => { - randomSeed.Value = 0; + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; }); AddAssert("rotation and scale restored", () => @@ -70,13 +70,6 @@ namespace osu.Game.Rulesets.Catch.Tests drawableBanana.InnerRotation == bananaRotation && drawableBanana.InnerScale == bananaScale && drawableBanana.AccentColour.Value == bananaColour); - - AddStep("change start time", () => - { - drawableFruit.HitObject.StartTime = 1000; - }); - - AddAssert("random seed is changed", () => randomSeed.Value == 1000); } private class TestDrawableFruit : DrawableFruit diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 9fd01b3717..3a5e08b3c3 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Objects private Color4 getBananaColour() { - switch (StatelessRNG.NextInt(3, RandomSeed.Value)) + switch (StatelessRNG.NextInt(3, RandomSeed)) { default: return new Color4(255, 240, 0, 255); diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index b9c5e42777..b86b3a7496 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -99,9 +99,9 @@ namespace osu.Game.Rulesets.Catch.Objects /// /// The seed value used for visual randomness such as fruit rotation. - /// By default, truncated to an integer is used. + /// The value is truncated to an integer. /// - public Bindable RandomSeed = new Bindable(); + public int RandomSeed => (int)StartTime; protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { @@ -117,10 +117,6 @@ namespace osu.Game.Rulesets.Catch.Objects protected CatchHitObject() { XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset); - StartTimeBindable.BindValueChanged(change => - { - RandomSeed.Value = (int)change.NewValue; - }, true); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 4e34dd2b90..8e9d80106b 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -24,11 +24,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { base.LoadComplete(); - RandomSeed.BindValueChanged(_ => - { - UpdateComboColour(); - UpdateInitialTransforms(); - }); + // start time affects the random seed which is used to determine the banana colour + StartTimeBindable.BindValueChanged(_ => UpdateComboColour()); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 510431ff69..86c1c7d0cd 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; - public Bindable RandomSeed = new Bindable(); + public int RandomSeed => HitObject?.RandomSeed ?? 0; protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject) : base(hitObject) @@ -32,14 +32,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables /// /// Get a random number in range [0,1) based on seed . /// - public float RandomSingle(int series) => StatelessRNG.NextSingle(RandomSeed.Value, series); + public float RandomSingle(int series) => StatelessRNG.NextSingle(RandomSeed, series); protected override void OnApply() { base.OnApply(); XBindable.BindTo(HitObject.XBindable); - RandomSeed.BindTo(HitObject.RandomSeed); } protected override void OnFree() @@ -47,7 +46,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.OnFree(); XBindable.UnbindFrom(HitObject.XBindable); - RandomSeed.UnbindFrom(HitObject.RandomSeed); } public Func CheckPosition; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 010a3ee08c..56b9b56372 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -36,11 +36,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables VisualRepresentation.BindValueChanged(_ => updatePiece()); HyperDash.BindValueChanged(_ => updatePiece(), true); + } - RandomSeed.BindValueChanged(_ => - { - ScaleContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; - }, true); + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); + + ScaleContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; } private void updatePiece() From 2e8195e059d8cd2f17d5477460797c4279eef86c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 3 Dec 2020 12:13:14 +0900 Subject: [PATCH 32/48] Use transformation to set fruit rotation --- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 56b9b56372..ef9df02a68 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -5,6 +5,7 @@ using System; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { base.UpdateInitialTransforms(); - ScaleContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; + ScaleContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); } private void updatePiece() From 897f593b379b10c2765b54ff22618b821c3ad77e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Dec 2020 13:26:28 +0900 Subject: [PATCH 33/48] Fix beatmap carousel panels getting masked away when out of scroll bounds Regressed in https://github.com/ppy/osu/pull/10973 due to removed masking specification. Closes #11067. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 4ce87927a1..d76f0abb9e 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -914,6 +914,9 @@ namespace osu.Game.Screens.Select { // size is determined by the carousel itself, due to not all content necessarily being loaded. ScrollContent.AutoSizeAxes = Axes.None; + + // the scroll container may get pushed off-screen by global screen changes, but we still want panels to display outside of the bounds. + Masking = false; } // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) From e3bbc2b1284ab2b7bdeff55843146ab86b4321b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 14:28:37 +0900 Subject: [PATCH 34/48] Rework osu! hidden mod to avoid storing hitobjects --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 134 +++++++++--------- .../Objects/Drawables/DrawableSliderHead.cs | 17 ++- .../Objects/Drawables/DrawableSliderRepeat.cs | 4 + .../Objects/Drawables/DrawableSliderTail.cs | 12 ++ 4 files changed, 97 insertions(+), 70 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 7c1dd46c02..78e759f0e0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -2,9 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -25,23 +26,19 @@ namespace osu.Game.Rulesets.Osu.Mods protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner); - public override void ApplyToDrawableHitObjects(IEnumerable drawables) + public override void ApplyToBeatmap(IBeatmap beatmap) { - foreach (var d in drawables) - d.HitObjectApplied += applyFadeInAdjustment; + base.ApplyToBeatmap(beatmap); - base.ApplyToDrawableHitObjects(drawables); - } + foreach (var obj in beatmap.HitObjects.OfType()) + applyFadeInAdjustment(obj); - private void applyFadeInAdjustment(DrawableHitObject hitObject) - { - if (!(hitObject is DrawableOsuHitObject d)) - return; - - d.HitObject.TimeFadeIn = d.HitObject.TimePreempt * fade_in_duration_multiplier; - - foreach (var nested in d.NestedHitObjects) - applyFadeInAdjustment(nested); + static void applyFadeInAdjustment(OsuHitObject osuObject) + { + osuObject.TimeFadeIn = osuObject.TimePreempt * fade_in_duration_multiplier; + foreach (var nested in osuObject.NestedHitObjects.OfType()) + applyFadeInAdjustment(nested); + } } protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) @@ -56,37 +53,27 @@ namespace osu.Game.Rulesets.Osu.Mods applyState(hitObject, false); } - private void applyState(DrawableHitObject drawable, bool increaseVisibility) + private void applyState(DrawableHitObject drawableObject, bool increaseVisibility) { - if (!(drawable is DrawableOsuHitObject d)) + if (!(drawableObject is DrawableOsuHitObject drawableOsuObject)) return; - var h = d.HitObject; + OsuHitObject hitObject = drawableOsuObject.HitObject; - var fadeOutStartTime = h.StartTime - h.TimePreempt + h.TimeFadeIn; - var fadeOutDuration = h.TimePreempt * fade_out_duration_multiplier; + (double startTime, double duration) fadeOut = getFadeOutParameters(drawableOsuObject); - // new duration from completed fade in to end (before fading out) - var longFadeDuration = h.GetEndTime() - fadeOutStartTime; - - switch (drawable) + switch (drawableObject) { - case DrawableSliderTail sliderTail: - // use stored values from head circle to achieve same fade sequence. - var tailFadeOutParameters = getFadeOutParametersFromSliderHead(h); - - using (drawable.BeginAbsoluteSequence(tailFadeOutParameters.startTime, true)) - sliderTail.FadeOut(tailFadeOutParameters.duration); + case DrawableSliderTail _: + using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true)) + drawableObject.FadeOut(fadeOut.duration); break; case DrawableSliderRepeat sliderRepeat: - // use stored values from head circle to achieve same fade sequence. - var repeatFadeOutParameters = getFadeOutParametersFromSliderHead(h); - - using (drawable.BeginAbsoluteSequence(repeatFadeOutParameters.startTime, true)) + using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true)) // only apply to circle piece – reverse arrow is not affected by hidden. - sliderRepeat.CirclePiece.FadeOut(repeatFadeOutParameters.duration); + sliderRepeat.CirclePiece.FadeOut(fadeOut.duration); break; @@ -101,29 +88,23 @@ namespace osu.Game.Rulesets.Osu.Mods else { // we don't want to see the approach circle - using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + using (circle.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt, true)) circle.ApproachCircle.Hide(); } - // fade out immediately after fade in. - using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) - fadeTarget.FadeOut(fadeOutDuration); + using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true)) + fadeTarget.FadeOut(fadeOut.duration); break; case DrawableSlider slider: - associateNestedSliderCirclesWithHead(slider.HitObject); - - using (slider.BeginAbsoluteSequence(fadeOutStartTime, true)) - slider.Body.FadeOut(longFadeDuration, Easing.Out); + using (slider.BeginAbsoluteSequence(fadeOut.startTime, true)) + slider.Body.FadeOut(fadeOut.duration, Easing.Out); break; case DrawableSliderTick sliderTick: - // slider ticks fade out over up to one second - var tickFadeOutDuration = Math.Min(sliderTick.HitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000); - - using (sliderTick.BeginAbsoluteSequence(sliderTick.HitObject.StartTime - tickFadeOutDuration, true)) - sliderTick.FadeOut(tickFadeOutDuration); + using (sliderTick.BeginAbsoluteSequence(fadeOut.startTime, true)) + sliderTick.FadeOut(fadeOut.duration); break; @@ -131,30 +112,55 @@ namespace osu.Game.Rulesets.Osu.Mods // hide elements we don't care about. // todo: hide background - using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true)) - spinner.FadeOut(fadeOutDuration); + using (spinner.BeginAbsoluteSequence(fadeOut.startTime, true)) + spinner.FadeOut(fadeOut.duration); break; } } - private readonly Dictionary correspondingSliderHeadForObject = new Dictionary(); - - private void associateNestedSliderCirclesWithHead(Slider slider) + private (double startTime, double duration) getFadeOutParameters(DrawableOsuHitObject drawableObject) { - var sliderHead = slider.NestedHitObjects.Single(obj => obj is SliderHeadCircle); - - foreach (var nested in slider.NestedHitObjects) + switch (drawableObject) { - if ((nested is SliderRepeat || nested is SliderEndCircle) && !correspondingSliderHeadForObject.ContainsKey(nested)) - correspondingSliderHeadForObject[nested] = (SliderHeadCircle)sliderHead; - } - } + case DrawableSliderTail tail: + // Use the same fade sequence as the slider head. + Debug.Assert(tail.Slider != null); + return getParameters(tail.Slider.HeadCircle); - private (double startTime, double duration) getFadeOutParametersFromSliderHead(OsuHitObject h) - { - var sliderHead = correspondingSliderHeadForObject[h]; - return (sliderHead.StartTime - sliderHead.TimePreempt + sliderHead.TimeFadeIn, sliderHead.TimePreempt * fade_out_duration_multiplier); + case DrawableSliderRepeat repeat: + // Use the same fade sequence as the slider head. + Debug.Assert(repeat.Slider != null); + return getParameters(repeat.Slider.HeadCircle); + + default: + return getParameters(drawableObject.HitObject); + } + + static (double startTime, double duration) getParameters(OsuHitObject hitObject) + { + var fadeOutStartTime = hitObject.StartTime - hitObject.TimePreempt + hitObject.TimeFadeIn; + var fadeOutDuration = hitObject.TimePreempt * fade_out_duration_multiplier; + + // new duration from completed fade in to end (before fading out) + var longFadeDuration = hitObject.GetEndTime() - fadeOutStartTime; + + switch (hitObject) + { + case Slider _: + return (fadeOutStartTime, longFadeDuration); + + case SliderTick _: + var tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000); + return (hitObject.StartTime - tickFadeOutDuration, tickFadeOutDuration); + + case Spinner _: + return (fadeOutStartTime + longFadeDuration, fadeOutDuration); + + default: + return (fadeOutStartTime, fadeOutDuration); + } + } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 3a92938d75..e878d61eec 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Drawables; @@ -11,14 +13,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderHead : DrawableHitCircle { + [CanBeNull] + public Slider Slider => drawableSlider?.HitObject; + private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; private DrawableSlider drawableSlider; - private Slider slider => drawableSlider?.HitObject; - public DrawableSliderHead() { } @@ -58,11 +61,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.Update(); - double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); + Debug.Assert(Slider != null); + + double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1); //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. if (!IsHit) - Position = slider.CurvePositionAt(completionProgress); + Position = Slider.CurvePositionAt(completionProgress); } public Action OnShake; @@ -71,8 +76,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void updatePosition() { - if (slider != null) - Position = HitObject.Position - slider.Position; + if (Slider != null) + Position = HitObject.Position - Slider.Position; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 0735d48ae1..ba503cca6a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,6 +19,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new SliderRepeat HitObject => (SliderRepeat)base.HitObject; + [CanBeNull] + public Slider Slider => drawableSlider?.HitObject; + private double animDuration; public Drawable CirclePiece { get; private set; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index eff72168ee..3deff55538 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,6 +16,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; + [CanBeNull] + public Slider Slider => drawableSlider?.HitObject; + /// /// The judgement text is provided by the . /// @@ -22,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool Tracking { get; set; } + private DrawableSlider drawableSlider; private SkinnableDrawable circlePiece; private Container scaleContainer; @@ -59,6 +64,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } + protected override void OnParentReceived(DrawableHitObject parent) + { + base.OnParentReceived(parent); + + drawableSlider = (DrawableSlider)parent; + } + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); From db7e82c5603b8df224a0c933ced3ae255aeac340 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 14:54:02 +0900 Subject: [PATCH 35/48] Add test --- .../Mods/TestSceneOsuModHidden.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index 40f1c4a52f..ff308f389f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -92,6 +92,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = checkSomeHit }); + [Test] + public void TestWithSliderReuse() => CreateModTest(new ModTestData + { + Mod = new OsuModHidden(), + Autoplay = true, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 1000, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + }, + new Slider + { + StartTime = 4000, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + }, + } + }, + PassCondition = checkSomeHit + }); + private bool checkSomeHit() { return Player.ScoreProcessor.JudgedHits >= 4; From 3550e5b30f93d965265bbf4ab611a588753a23b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Dec 2020 16:42:06 +0900 Subject: [PATCH 36/48] Add length display to room screen as well --- .../Online/API/Requests/Responses/APIBeatmap.cs | 1 + .../Online/Multiplayer/PlaylistExtensions.cs | 16 ++++++++++++++++ .../Match/Components/MatchSettingsOverlay.cs | 9 ++------- osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 5 ++++- 4 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/PlaylistExtensions.cs diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index ae65ac09b2..7343870dbc 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -75,6 +75,7 @@ namespace osu.Game.Online.API.Requests.Responses StarDifficulty = starDifficulty, OnlineBeatmapID = OnlineBeatmapID, Version = version, + // this is actually an incorrect mapping (Length is calculated as drain length in lazer's import process, see BeatmapManager.calculateLength). Length = TimeSpan.FromSeconds(length).TotalMilliseconds, Status = Status, BeatmapSet = set, diff --git a/osu.Game/Online/Multiplayer/PlaylistExtensions.cs b/osu.Game/Online/Multiplayer/PlaylistExtensions.cs new file mode 100644 index 0000000000..fe3d96e295 --- /dev/null +++ b/osu.Game/Online/Multiplayer/PlaylistExtensions.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 System.Linq; +using Humanizer; +using Humanizer.Localisation; +using osu.Framework.Bindables; + +namespace osu.Game.Online.Multiplayer +{ + public static class PlaylistExtensions + { + public static string GetTotalDuration(this BindableList playlist) => + playlist.Select(p => p.Beatmap.Value.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index 8bf66b084c..668a373d80 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Specialized; -using System.Linq; using Humanizer; -using Humanizer.Localisation; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -339,11 +337,8 @@ namespace osu.Game.Screens.Multi.Match.Components ApplyButton.Enabled.Value = hasValidSettings; } - private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) - { - double totalLength = Playlist.Select(p => p.Beatmap.Value.Length).Sum(); - playlistLength.Text = $"Length: {totalLength.Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2)}"; - } + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => + playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 2cbe215a39..13a5d89a12 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -57,6 +57,7 @@ namespace osu.Game.Screens.Multi.Match private IBindable> managerUpdated; private OverlinedHeader participantsHeader; + private OverlinedHeader playlistHeader; public MatchSubScreen(Room room) { @@ -135,7 +136,7 @@ namespace osu.Game.Screens.Multi.Match RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] { new OverlinedHeader("Playlist"), }, + new Drawable[] { playlistHeader = new OverlinedHeader("Playlist"), }, new Drawable[] { new DrawableRoomPlaylistWithResults @@ -243,6 +244,8 @@ namespace osu.Game.Screens.Multi.Match managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); + + playlist.BindCollectionChanged((_, __) => playlistHeader.Details.Value = playlist.GetTotalDuration(), true); } public override bool OnExiting(IScreen next) From c64343c7d719ed8c5c3dc1bfa35934145ab2ffad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Dec 2020 17:42:41 +0900 Subject: [PATCH 37/48] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 9d99218f88..9a3d42d6b7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4b931726e0..9d37ceee6c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 3a47b77820..ab03393836 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 78c43641d1c6022e1143280843543fec65b678f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Dec 2020 17:43:09 +0900 Subject: [PATCH 38/48] Update imagesharp namespaces (and consume System.Drawing types instead) --- osu.Game.Tournament/Models/TournamentMatch.cs | 2 +- osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs | 2 +- .../Screens/Ladder/Components/DrawableTournamentMatch.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Models/TournamentMatch.cs b/osu.Game.Tournament/Models/TournamentMatch.cs index 8ebcbf4e15..bdfb1728f3 100644 --- a/osu.Game.Tournament/Models/TournamentMatch.cs +++ b/osu.Game.Tournament/Models/TournamentMatch.cs @@ -4,10 +4,10 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Drawing; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Tournament.Screens.Ladder.Components; -using SixLabors.Primitives; namespace osu.Game.Tournament.Models { diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index efec4cffdd..ca46c3b050 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Drawing; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -16,7 +17,6 @@ using osu.Game.Tournament.Screens.Ladder; using osu.Game.Tournament.Screens.Ladder.Components; using osuTK; using osuTK.Graphics; -using SixLabors.Primitives; namespace osu.Game.Tournament.Screens.Editors { diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index f2065e7e88..1c805bb42e 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Drawing; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -13,7 +14,6 @@ using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; using osuTK.Input; -using SixLabors.Primitives; namespace osu.Game.Tournament.Screens.Ladder.Components { From 3e62da119eca2d34c3e24b1bf61ec68a6eeaddf9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 17:59:39 +0900 Subject: [PATCH 39/48] Add to inspector also --- .../Components/OverlinedPlaylistHeader.cs | 22 +++++++++++++++++++ .../Multi/Lounge/Components/RoomInspector.cs | 2 +- .../Screens/Multi/Match/MatchSubScreen.cs | 5 +---- 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs diff --git a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs new file mode 100644 index 0000000000..02dbe501b1 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.Multi.Components +{ + public class OverlinedPlaylistHeader : OverlinedHeader + { + public OverlinedPlaylistHeader() + : base("Playlist") + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playlist.BindCollectionChanged((_, __) => Details.Value = Playlist.GetTotalDuration()); + } + } +} diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs index 77fbd606f4..dfee278e87 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components } } }, - new Drawable[] { new OverlinedHeader("Playlist"), }, + new Drawable[] { new OverlinedPlaylistHeader(), }, new Drawable[] { new DrawableRoomPlaylist(false, false) diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 13a5d89a12..2f8aad4e65 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -57,7 +57,6 @@ namespace osu.Game.Screens.Multi.Match private IBindable> managerUpdated; private OverlinedHeader participantsHeader; - private OverlinedHeader playlistHeader; public MatchSubScreen(Room room) { @@ -136,7 +135,7 @@ namespace osu.Game.Screens.Multi.Match RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] { playlistHeader = new OverlinedHeader("Playlist"), }, + new Drawable[] { new OverlinedPlaylistHeader(), }, new Drawable[] { new DrawableRoomPlaylistWithResults @@ -244,8 +243,6 @@ namespace osu.Game.Screens.Multi.Match managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); - - playlist.BindCollectionChanged((_, __) => playlistHeader.Details.Value = playlist.GetTotalDuration(), true); } public override bool OnExiting(IScreen next) From 62b1e37f73701a71992d23b4d987f6f5854a7fae Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 18:04:53 +0900 Subject: [PATCH 40/48] Use async overloads --- osu.Game/Graphics/ScreenshotManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index d1f6fd445e..53ee711626 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -116,13 +116,13 @@ namespace osu.Game.Graphics switch (screenshotFormat.Value) { case ScreenshotFormat.Png: - image.SaveAsPng(stream); + await image.SaveAsPngAsync(stream); break; case ScreenshotFormat.Jpg: const int jpeg_quality = 92; - image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality }); + await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }); break; default: From 8245bb85dc93970e584d8b821769fb8eda7b7c5f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 18:06:55 +0900 Subject: [PATCH 41/48] Invoke on initial bind --- osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs index 02dbe501b1..5552c1cb72 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs +++ b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Multi.Components { base.LoadComplete(); - Playlist.BindCollectionChanged((_, __) => Details.Value = Playlist.GetTotalDuration()); + Playlist.BindCollectionChanged((_, __) => Details.Value = Playlist.GetTotalDuration(), true); } } } From 73e99718bc23f52854bbc0eb467dcb63bae10a97 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 19:46:42 +0900 Subject: [PATCH 42/48] Change order of OnParentReceived() --- .../Objects/Drawables/DrawableSliderHead.cs | 5 +++++ .../Objects/Drawables/DrawableSliderRepeat.cs | 5 +++++ .../Objects/Drawables/DrawableSliderTick.cs | 10 +++++++++- .../Objects/Drawables/DrawableSpinnerTick.cs | 1 + .../Objects/Drawables/DrawableHitObject.cs | 16 ++++++++++++---- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs | 3 ++- osu.Game/Rulesets/UI/Playfield.cs | 4 +++- 8 files changed, 38 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 3a92938d75..d1928bd4bb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -47,6 +47,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnParentReceived(parent); drawableSlider = (DrawableSlider)parent; + } + + protected override void OnApply() + { + base.OnApply(); pathVersion.BindTo(drawableSlider.PathVersion); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 0735d48ae1..f368615e77 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -65,6 +65,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnParentReceived(parent); drawableSlider = (DrawableSlider)parent; + } + + protected override void OnApply() + { + base.OnApply(); Position = HitObject.Position - drawableSlider.Position; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index faccf5d4d1..d40b6aea6e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; private SkinnableDrawable scaleContainer; + private DrawableSlider drawableSlider; public DrawableSliderTick() : base(null) @@ -66,7 +67,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.OnParentReceived(parent); - Position = HitObject.Position - ((DrawableSlider)parent).HitObject.Position; + drawableSlider = (DrawableSlider)parent; + } + + protected override void OnApply() + { + base.OnApply(); + + Position = HitObject.Position - drawableSlider.HitObject.Position; } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index f37d933e11..d10c4f7511 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void OnParentReceived(DrawableHitObject parent) { base.OnParentReceived(parent); + drawableSpinner = (DrawableSpinner)parent; } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index a922da0aa9..37c36ace7b 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -230,12 +230,12 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in HitObject.NestedHitObjects) { - var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h); + var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this); var drawableNested = pooledDrawableNested ?? CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); - // Invoke the event only if this nested object is just created by `CreateNestedHitObject`. + // Only invoke the event for non-pooled DHOs, otherwise the event will be fired by the playfield. if (pooledDrawableNested == null) OnNestedDrawableCreated?.Invoke(drawableNested); @@ -243,10 +243,12 @@ namespace osu.Game.Rulesets.Objects.Drawables drawableNested.OnRevertResult += onRevertResult; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; + // ApplyParent() should occur before Apply() in all cases, so it's invoked before the nested DHO is added to the hierarchy below, but after its events are initialised. + if (pooledDrawableNested == null) + drawableNested.ApplyParent(this); + nestedHitObjects.Value.Add(drawableNested); AddNestedHitObject(drawableNested); - - drawableNested.OnParentReceived(this); } StartTimeBindable.BindTo(HitObject.StartTimeBindable); @@ -348,6 +350,12 @@ namespace osu.Game.Rulesets.Objects.Drawables { } + /// + /// Applies a parenting to this . + /// + /// The parenting . + public void ApplyParent(DrawableHitObject parent) => OnParentReceived(parent); + /// /// Invoked when this receives a new parenting . /// diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index ac5d281ddc..12e39d4fbf 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI { Debug.Assert(!drawableMap.ContainsKey(entry)); - var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject); + var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null); if (drawable == null) throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); diff --git a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs index 315926dfc6..2d700076d6 100644 --- a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs +++ b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs @@ -13,8 +13,9 @@ namespace osu.Game.Rulesets.UI /// Attempts to retrieve the poolable representation of a . /// /// The to retrieve the representation of. + /// The parenting , if any. /// The representing , or null if no poolable representation exists. [CanBeNull] - DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject); + DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject, [CanBeNull] DrawableHitObject parent); } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index a2ac234471..01b25c9717 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.UI AddInternal(pool); } - DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject) + DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject, DrawableHitObject parent) { var lookupType = hitObject.GetType(); @@ -359,6 +359,8 @@ namespace osu.Game.Rulesets.UI if (!lifetimeEntryMap.TryGetValue(hitObject, out var entry)) lifetimeEntryMap[hitObject] = entry = CreateLifetimeEntry(hitObject); + if (parent != null) + dho.ApplyParent(parent); dho.Apply(hitObject, entry); }); } From 0bdf99b97a960cd9a5c58e72b4bf3bb0af984ea3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 20:03:39 +0900 Subject: [PATCH 43/48] Remove OnParentReceived() --- .../Objects/Drawables/DrawableSliderHead.cs | 22 +++++---------- .../Objects/Drawables/DrawableSliderRepeat.cs | 17 ++++-------- .../Objects/Drawables/DrawableSliderTick.cs | 12 +++------ .../Objects/Drawables/DrawableSpinnerTick.cs | 15 +++-------- .../Objects/Drawables/DrawableHitObject.cs | 27 +++++++------------ osu.Game/Rulesets/UI/Playfield.cs | 3 +-- 6 files changed, 29 insertions(+), 67 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index d1928bd4bb..f584c9c2d3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -4,20 +4,19 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderHead : DrawableHitCircle { + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; - private DrawableSlider drawableSlider; - - private Slider slider => drawableSlider?.HitObject; + private Slider slider => DrawableSlider?.HitObject; public DrawableSliderHead() { @@ -39,24 +38,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.OnFree(); - pathVersion.UnbindFrom(drawableSlider.PathVersion); - } - - protected override void OnParentReceived(DrawableHitObject parent) - { - base.OnParentReceived(parent); - - drawableSlider = (DrawableSlider)parent; + pathVersion.UnbindFrom(DrawableSlider.PathVersion); } protected override void OnApply() { base.OnApply(); - pathVersion.BindTo(drawableSlider.PathVersion); + pathVersion.BindTo(DrawableSlider.PathVersion); - OnShake = drawableSlider.Shake; - CheckHittable = (d, t) => drawableSlider.CheckHittable?.Invoke(d, t) ?? true; + OnShake = DrawableSlider.Shake; + CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true; } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index f368615e77..2fd9af894d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new SliderRepeat HitObject => (SliderRepeat)base.HitObject; + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + private double animDuration; public Drawable CirclePiece { get; private set; } @@ -26,8 +28,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; - private DrawableSlider drawableSlider; - public DrawableSliderRepeat() : base(null) { @@ -60,24 +60,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } - protected override void OnParentReceived(DrawableHitObject parent) - { - base.OnParentReceived(parent); - - drawableSlider = (DrawableSlider)parent; - } - protected override void OnApply() { base.OnApply(); - Position = HitObject.Position - drawableSlider.Position; + Position = HitObject.Position - DrawableSlider.Position; } protected override void CheckForResult(bool userTriggered, double timeOffset) { if (HitObject.StartTime <= Time.Current) - ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = DrawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void UpdateInitialTransforms() @@ -119,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (IsHit) return; bool isRepeatAtEnd = HitObject.RepeatIndex % 2 == 0; - List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; + List curve = ((PlaySliderBody)DrawableSlider.Body.Drawable).CurrentCurve; Position = isRepeatAtEnd ? end : start; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index d40b6aea6e..c7bfdb02fb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -22,8 +22,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + private SkinnableDrawable scaleContainer; - private DrawableSlider drawableSlider; public DrawableSliderTick() : base(null) @@ -63,18 +64,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } - protected override void OnParentReceived(DrawableHitObject parent) - { - base.OnParentReceived(parent); - - drawableSlider = (DrawableSlider)parent; - } - protected override void OnApply() { base.OnApply(); - Position = HitObject.Position - drawableSlider.HitObject.Position; + Position = HitObject.Position - DrawableSlider.HitObject.Position; } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index d10c4f7511..726fbd3ea6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -1,14 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Objects.Drawables; - namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject { public override bool DisplayResult => false; + protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject; + public DrawableSpinnerTick() : base(null) { @@ -19,16 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } - private DrawableSpinner drawableSpinner; - - protected override void OnParentReceived(DrawableHitObject parent) - { - base.OnParentReceived(parent); - - drawableSpinner = (DrawableSpinner)parent; - } - - protected override double MaximumJudgementOffset => drawableSpinner.HitObject.Duration; + protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration; /// /// Apply a judgement result. diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 37c36ace7b..94d63e4e68 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -43,6 +43,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public HitObject HitObject { get; private set; } + /// + /// The parenting , if any. + /// + [CanBeNull] + protected internal DrawableHitObject ParentHitObject { get; internal set; } + /// /// The colour used for various elements of this DrawableHitObject. /// @@ -243,9 +249,9 @@ namespace osu.Game.Rulesets.Objects.Drawables drawableNested.OnRevertResult += onRevertResult; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; - // ApplyParent() should occur before Apply() in all cases, so it's invoked before the nested DHO is added to the hierarchy below, but after its events are initialised. - if (pooledDrawableNested == null) - drawableNested.ApplyParent(this); + // This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation(). + // Must be done before the nested DHO is added to occur before the nested Apply()! + drawableNested.ParentHitObject = this; nestedHitObjects.Value.Add(drawableNested); AddNestedHitObject(drawableNested); @@ -317,6 +323,7 @@ namespace osu.Game.Rulesets.Objects.Drawables OnFree(); HitObject = null; + ParentHitObject = null; Result = null; lifetimeEntry = null; @@ -350,20 +357,6 @@ namespace osu.Game.Rulesets.Objects.Drawables { } - /// - /// Applies a parenting to this . - /// - /// The parenting . - public void ApplyParent(DrawableHitObject parent) => OnParentReceived(parent); - - /// - /// Invoked when this receives a new parenting . - /// - /// The parenting . - protected virtual void OnParentReceived(DrawableHitObject parent) - { - } - /// /// Invoked by the base to populate samples, once on initial load and potentially again on any change to the samples collection. /// diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 01b25c9717..cbf3362ea7 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -359,8 +359,7 @@ namespace osu.Game.Rulesets.UI if (!lifetimeEntryMap.TryGetValue(hitObject, out var entry)) lifetimeEntryMap[hitObject] = entry = CreateLifetimeEntry(hitObject); - if (parent != null) - dho.ApplyParent(parent); + dho.ParentHitObject = parent; dho.Apply(hitObject, entry); }); } From c25e2c3dd577e7d5dc1ce3765ac0d6bec40aca21 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 3 Dec 2020 23:13:14 +0200 Subject: [PATCH 44/48] Select recommended beatmap if last selection is filtered --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 7935debac7..bf045ed612 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Select.Carousel protected override CarouselItem GetNextToSelect() { - if (LastSelected == null) + if (LastSelected == null || LastSelected.Filtered.Value) { if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)) is BeatmapInfo recommended) return Children.OfType().First(b => b.Beatmap == recommended); From 0bc591fef2dc5ee21680b0f361bb3388af35a825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Dec 2020 22:38:51 +0100 Subject: [PATCH 45/48] Add failing assertions `GameplayBeatmap` has to be used instead of the normal bindable `Beatmap`, beecause the former uses osu!-specific hitobjects, while the latter returns convert objects (i.e. `ConvertSlider`s). Similarly, the mod has to be fetched from the player instead of the global bindable, as `Player` has its own cloned instance of the mod, to which the beatmap is applied. The global bindable instance does not have `FirstObject` set. --- .../Mods/TestSceneOsuModHidden.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index ff308f389f..1ac3ad9194 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -2,12 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -17,15 +20,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [Test] public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData { - Mod = new OsuModHidden(), + Mod = new TestOsuModHidden(), Autoplay = true, - PassCondition = checkSomeHit + PassCondition = () => checkSomeHit() && objectWithIncreasedVisibilityHasIndex(0) }); [Test] public void FirstCircleAfterTwoSpinners() => CreateModTest(new ModTestData { - Mod = new OsuModHidden(), + Mod = new TestOsuModHidden(), Autoplay = true, Beatmap = new Beatmap { @@ -54,13 +57,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } } }, - PassCondition = checkSomeHit + PassCondition = () => checkSomeHit() && objectWithIncreasedVisibilityHasIndex(2) }); [Test] public void FirstSliderAfterTwoSpinners() => CreateModTest(new ModTestData { - Mod = new OsuModHidden(), + Mod = new TestOsuModHidden(), Autoplay = true, Beatmap = new Beatmap { @@ -89,13 +92,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } } }, - PassCondition = checkSomeHit + PassCondition = () => checkSomeHit() && objectWithIncreasedVisibilityHasIndex(2) }); [Test] public void TestWithSliderReuse() => CreateModTest(new ModTestData { - Mod = new OsuModHidden(), + Mod = new TestOsuModHidden(), Autoplay = true, Beatmap = new Beatmap { @@ -116,9 +119,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = checkSomeHit }); - private bool checkSomeHit() + private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4; + + private bool objectWithIncreasedVisibilityHasIndex(int index) + => Player.Mods.Value.OfType().Single().FirstObject == Player.ChildrenOfType().Single().HitObjects[index]; + + private class TestOsuModHidden : OsuModHidden { - return Player.ScoreProcessor.JudgedHits >= 4; + public new HitObject FirstObject => base.FirstObject; } } } From 4d739f11a8352ed16cd7dbb2f499cee9ad6d6240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Dec 2020 22:40:30 +0100 Subject: [PATCH 46/48] Fix spinner ticks getting increased visibility state Regressed in #10696. The old `IsFirstHideableObject()` method did not consider nested hitobjects, while its replacement - `IsFirstAdjustableObject()` - did. Therefore, spinner ticks could be considered first adjustable objects, breaking the old logic. There is no need to match over `SpinnerBonusTick`, as it inherits from `SpinnerTick`. --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 78e759f0e0..45f314af7b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods private const double fade_in_duration_multiplier = 0.4; private const double fade_out_duration_multiplier = 0.3; - protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner); + protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick); public override void ApplyToBeatmap(IBeatmap beatmap) { From 71fa0da7f4dc1b57700e68277e4fed251b22df71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Dec 2020 23:13:48 +0100 Subject: [PATCH 47/48] Add failing test cases --- .../Audio/SampleInfoEqualityTest.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 osu.Game.Tests/Audio/SampleInfoEqualityTest.cs diff --git a/osu.Game.Tests/Audio/SampleInfoEqualityTest.cs b/osu.Game.Tests/Audio/SampleInfoEqualityTest.cs new file mode 100644 index 0000000000..149096608f --- /dev/null +++ b/osu.Game.Tests/Audio/SampleInfoEqualityTest.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Audio; + +namespace osu.Game.Tests.Audio +{ + [TestFixture] + public class SampleInfoEqualityTest + { + [Test] + public void TestSameSingleSamplesAreEqual() + { + var first = new SampleInfo("sample"); + var second = new SampleInfo("sample"); + + assertEquality(first, second); + } + + [Test] + public void TestDifferentSingleSamplesAreNotEqual() + { + var first = new SampleInfo("first"); + var second = new SampleInfo("second"); + + assertNonEquality(first, second); + } + + [Test] + public void TestDifferentCountSampleSetsAreNotEqual() + { + var first = new SampleInfo("sample", "extra"); + var second = new SampleInfo("sample"); + + assertNonEquality(first, second); + } + + [Test] + public void TestDifferentSampleSetsOfSameCountAreNotEqual() + { + var first = new SampleInfo("first", "common"); + var second = new SampleInfo("common", "second"); + + assertNonEquality(first, second); + } + + [Test] + public void TestSameOrderSameSampleSetsAreEqual() + { + var first = new SampleInfo("first", "second"); + var second = new SampleInfo("first", "second"); + + assertEquality(first, second); + } + + [Test] + public void TestDifferentOrderSameSampleSetsAreEqual() + { + var first = new SampleInfo("first", "second"); + var second = new SampleInfo("second", "first"); + + assertEquality(first, second); + } + + private void assertEquality(SampleInfo first, SampleInfo second) + { + Assert.That(first.Equals(second), Is.True); + Assert.That(first.GetHashCode(), Is.EqualTo(second.GetHashCode())); + } + + private void assertNonEquality(SampleInfo first, SampleInfo second) + { + Assert.That(first.Equals(second), Is.False); + Assert.That(first.GetHashCode(), Is.Not.EqualTo(second.GetHashCode())); + } + } +} From 15d9147eddb9a39a4569d53042fcc05f1d5cf62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Dec 2020 23:19:26 +0100 Subject: [PATCH 48/48] Ensure equality member consistency for SampleInfo The previous implementation of `SampleInfo`'s equality members was not completely correct in its treatment of the `sampleNames` array. While `Equals()` compared the values of `sampleNames` using `SequenceEqual()`, therefore performing a structural check that inspects the contents of both arrays, `GetHashCode()` used `HashCode.Combine()` directly on the arrays, therefore operating on reference equality. This could cause the pooling mechanism of samples to fail, as pointed out in #11079. To resolve, change the `GetHashCode()` implementation such that it also considers the contents of the array rather than just the reference to the array itself. This is achieved by leveraging `StructuralEqualityComparer`. Additionally, as a bonus, an array sort was added to the constructor of `SampleInfo`. This is intended to be a "canonicalisation" processing step for the array of sample names. Thanks to that sort, two instances of `SampleInfo` that have the same sample names but permutated will also turn out to be equal and have the same hash codes, given the implementation of both equality members. This gives `SampleInfo` set-like semantics. --- osu.Game/Audio/SampleInfo.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index 221bc31639..5d8240204e 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections; using System.Collections.Generic; using System.Linq; @@ -17,6 +18,7 @@ namespace osu.Game.Audio public SampleInfo(params string[] sampleNames) { this.sampleNames = sampleNames; + Array.Sort(sampleNames); } public IEnumerable LookupNames => sampleNames; @@ -25,7 +27,9 @@ namespace osu.Game.Audio public override int GetHashCode() { - return HashCode.Combine(sampleNames, Volume); + return HashCode.Combine( + StructuralComparisons.StructuralEqualityComparer.GetHashCode(sampleNames), + Volume); } public bool Equals(SampleInfo other)