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.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/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs new file mode 100644 index 0000000000..2ffebb7de1 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -0,0 +1,96 @@ +// 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.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Tests.Visual; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneFruitRandomness : OsuTestScene + { + private readonly TestDrawableFruit drawableFruit; + private readonly TestDrawableBanana drawableBanana; + + public TestSceneFruitRandomness() + { + drawableFruit = new TestDrawableFruit(new Fruit()); + drawableBanana = new TestDrawableBanana(new Banana()); + + Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 }); + Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana)); + + 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("Initialize start time", () => + { + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; + + fruitRotation = drawableFruit.InnerRotation; + bananaRotation = drawableBanana.InnerRotation; + bananaScale = drawableBanana.InnerScale; + bananaColour = drawableBanana.AccentColour.Value; + }); + + AddStep("change start time", () => + { + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; + }); + + 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 start time", () => + { + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; + }); + + AddAssert("rotation and scale restored", () => + drawableFruit.InnerRotation == fruitRotation && + drawableBanana.InnerRotation == bananaRotation && + drawableBanana.InnerScale == bananaScale && + drawableBanana.AccentColour.Value == bananaColour); + } + + 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) + { + } + } + } +} 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 }, })))); diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index a274f25200..3f71da713e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -5,7 +5,6 @@ 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; @@ -31,17 +30,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)) { 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..b86b3a7496 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. + /// The value is truncated to an integer. + /// + public int RandomSeed => (int)StartTime; + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index fb982bbdab..8e9d80106b 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,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { } + protected override void LoadComplete() + { + base.LoadComplete(); + + // start time affects the random seed which is used to determine the banana colour + StartTimeBindable.BindValueChanged(_ => UpdateComboColour()); + } + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); @@ -28,14 +35,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/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index c3dbfc325f..6aa8ff439e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Utils; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -18,12 +19,19 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; + public int RandomSeed => HitObject?.RandomSeed ?? 0; + protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject) : base(hitObject) { Anchor = Anchor.BottomLeft; } + /// + /// Get a random number in range [0,1) based on seed . + /// + public float RandomSingle(int series) => StatelessRNG.NextSingle(RandomSeed, series); + protected override void OnApply() { base.OnApply(); 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..ef9df02a68 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -5,7 +5,7 @@ using System; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Utils; +using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -30,8 +30,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); @@ -41,6 +39,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables HyperDash.BindValueChanged(_ => updatePiece(), true); } + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); + + ScaleContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); + } + private void updatePiece() { ScaleContainer.Child = new SkinnableDrawable( diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index 40f1c4a52f..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,12 +92,41 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } } }, + PassCondition = () => checkSomeHit() && objectWithIncreasedVisibilityHasIndex(2) + }); + + [Test] + public void TestWithSliderReuse() => CreateModTest(new ModTestData + { + Mod = new TestOsuModHidden(), + 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() + 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; } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 7c1dd46c02..45f314af7b 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; @@ -23,25 +24,21 @@ 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 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/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index d3787585e6..af5b609ec8 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,27 +101,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnFree(); 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) { var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide"); - samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone) - { - Looping = true - }); + slidingSample.Samples = new ISampleInfo[] { clone }; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 3a92938d75..acc95ab036 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -2,23 +2,25 @@ // 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; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderHead : DrawableHitCircle { + [CanBeNull] + public Slider Slider => DrawableSlider?.HitObject; + + 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; - public DrawableSliderHead() { } @@ -39,30 +41,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.OnFree(); - pathVersion.UnbindFrom(drawableSlider.PathVersion); + pathVersion.UnbindFrom(DrawableSlider.PathVersion); } - protected override void OnParentReceived(DrawableHitObject parent) + protected override void OnApply() { - base.OnParentReceived(parent); + base.OnApply(); - drawableSlider = (DrawableSlider)parent; + 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() { 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 +73,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..a684df98cb 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,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new SliderRepeat HitObject => (SliderRepeat)base.HitObject; + [CanBeNull] + public Slider Slider => DrawableSlider?.HitObject; + + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + private double animDuration; public Drawable CirclePiece { get; private set; } @@ -26,8 +32,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; - private DrawableSlider drawableSlider; - public DrawableSliderRepeat() : base(null) { @@ -60,19 +64,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } - protected override void OnParentReceived(DrawableHitObject parent) + protected override void OnApply() { - base.OnParentReceived(parent); + base.OnApply(); - drawableSlider = (DrawableSlider)parent; - - 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() @@ -114,7 +116,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/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index eff72168ee..6a8e02e886 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,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; + [CanBeNull] + public Slider Slider => DrawableSlider?.HitObject; + + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + /// /// The judgement text is provided by the . /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index faccf5d4d1..c7bfdb02fb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + private SkinnableDrawable scaleContainer; public DrawableSliderTick() @@ -62,11 +64,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } - protected override void OnParentReceived(DrawableHitObject parent) + protected override void OnApply() { - base.OnParentReceived(parent); + base.OnApply(); - Position = HitObject.Position - ((DrawableSlider)parent).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/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 5a11265a47..aea37acf6f 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,29 +101,28 @@ 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() + { + base.OnFree(); + + spinningSample.Samples = null; + } + protected override void LoadSamples() { base.LoadSamples(); - samplesContainer.Clear(); - spinningSample = null; - var firstSample = HitObject.Samples.FirstOrDefault(); if (firstSample != null) { var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("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; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index f37d933e11..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,15 +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.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())); + } + } +} 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 { diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index 240d70c418..5d8240204e 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -1,24 +1,41 @@ // 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; 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; public SampleInfo(params string[] sampleNames) { this.sampleNames = sampleNames; + Array.Sort(sampleNames); } public IEnumerable LookupNames => sampleNames; public int Volume { get; } = 100; + + public override int GetHashCode() + { + return HashCode.Combine( + StructuralComparisons.StructuralEqualityComparer.GetHashCode(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/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: 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/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index a922da0aa9..d800758cc1 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; @@ -43,6 +42,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. /// @@ -150,8 +155,6 @@ namespace osu.Game.Rulesets.Objects.Drawables [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } - private Container samplesContainer; - /// /// Whether the initialization logic in has applied. /// @@ -175,7 +178,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()); } protected override void LoadAsyncComplete() @@ -230,12 +233,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 +246,12 @@ namespace osu.Game.Rulesets.Objects.Drawables drawableNested.OnRevertResult += onRevertResult; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; + // 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); - - drawableNested.OnParentReceived(this); } StartTimeBindable.BindTo(HitObject.StartTimeBindable); @@ -297,6 +302,9 @@ 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; + // Release the samples for other hitobjects to use. + Samples.Samples = null; + if (nestedHitObjects.IsValueCreated) { foreach (var obj in nestedHitObjects.Value) @@ -315,6 +323,7 @@ namespace osu.Game.Rulesets.Objects.Drawables OnFree(); HitObject = null; + ParentHitObject = null; Result = null; lifetimeEntry = null; @@ -348,22 +357,11 @@ namespace osu.Game.Rulesets.Objects.Drawables { } - /// - /// 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. /// protected virtual void LoadSamples() { - samplesContainer.Clear(); - Samples = null; - var samples = GetSamples().ToArray(); if (samples.Length <= 0) @@ -375,7 +373,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/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..b4e0025351 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -8,20 +8,24 @@ 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; using System.Diagnostics; 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. @@ -81,6 +85,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 . /// @@ -97,9 +107,6 @@ namespace osu.Game.Rulesets.UI })); } - [Resolved(CanBeNull = true)] - private IReadOnlyList mods { get; set; } - [BackgroundDependencyLoader] private void load() { @@ -323,7 +330,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,10 +366,34 @@ namespace osu.Game.Rulesets.UI if (!lifetimeEntryMap.TryGetValue(hitObject, out var entry)) lifetimeEntryMap[hitObject] = entry = CreateLifetimeEntry(hitObject); + dho.ParentHitObject = parent; dho.Apply(hitObject, entry); }); } + private readonly Dictionary> samplePools = new Dictionary>(); + + public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) + { + if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) + AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1)); + + 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/Screens/Multi/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs new file mode 100644 index 0000000000..5552c1cb72 --- /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(), true); + } + } +} 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/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index caefc194b1..668a373d80 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Specialized; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -69,6 +70,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 +231,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 +254,7 @@ namespace osu.Game.Screens.Multi.Match.Components { new Dimension(), new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), } } }, @@ -315,6 +327,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 +337,9 @@ namespace osu.Game.Screens.Multi.Match.Components ApplyButton.Enabled.Value = hasValidSettings; } + 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; private void apply() diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 2cbe215a39..2f8aad4e65 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Multi.Match RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] { new OverlinedHeader("Playlist"), }, + new Drawable[] { new OverlinedPlaylistHeader(), }, new Drawable[] { new DrawableRoomPlaylistWithResults 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) 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); diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs new file mode 100644 index 0000000000..40193d1a1a --- /dev/null +++ b/osu.Game/Skinning/IPooledSampleProvider.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 JetBrains.Annotations; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + /// + /// 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/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 4f09aec0b6..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,13 +15,17 @@ namespace osu.Game.Skinning { protected bool RequestedPlaying { get; private set; } - public PausableSkinnableSound(ISampleInfo hitSamples) - : base(hitSamples) + public PausableSkinnableSound() { } - public PausableSkinnableSound(IEnumerable hitSamples) - : base(hitSamples) + public PausableSkinnableSound([NotNull] IEnumerable samples) + : base(samples) + { + } + + public PausableSkinnableSound([NotNull] ISampleInfo sample) + : base(sample) { } diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs new file mode 100644 index 0000000000..19b96d6c60 --- /dev/null +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -0,0 +1,168 @@ +// 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 JetBrains.Annotations; +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.Framework.Graphics.Containers; +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 + { + /// + /// The currently-loaded . + /// + [CanBeNull] + public DrawableSample Sample { get; private set; } + + private readonly AudioContainer sampleContainer; + private ISampleInfo sampleInfo; + + [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) + throw new InvalidOperationException($"A {nameof(PoolableSkinnableSample)} cannot be applied multiple {nameof(ISampleInfo)}s."); + + this.sampleInfo = sampleInfo; + + Volume.Value = sampleInfo.Volume / 100.0; + + if (LoadState >= LoadState.Ready) + updateSample(); + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateSample(); + } + + private void updateSample() + { + if (sampleInfo == null) + return; + + bool wasPlaying = Playing; + + sampleContainer.Clear(); + Sample = null; + + 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; + + 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(); + } + + /// + /// Plays the sample. + /// + /// Whether to play the sample from the beginning. + public void Play(bool restart = true) => Sample?.Play(restart); + + /// + /// Stops the sample. + /// + public void Stop() => Sample?.Stop(); + + /// + /// Whether the sample is currently playing. + /// + public bool Playing => Sample?.Playing ?? false; + + private bool looping; + + /// + /// Whether the sample should loop on completion. + /// + public bool Looping + { + get => looping; + set + { + looping = value; + + if (Sample != null) + Sample.Looping = value; + } + } + + #region Re-expose AudioContainer + + public BindableNumber Volume => sampleContainer.Volume; + + public BindableNumber Balance => sampleContainer.Balance; + + public BindableNumber Frequency => sampleContainer.Frequency; + + public BindableNumber Tempo => sampleContainer.Tempo; + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable); + + public void RemoveAllAdjustments(AdjustableProperty type) => sampleContainer.RemoveAllAdjustments(type); + + public IBindable AggregateVolume => sampleContainer.AggregateVolume; + + public IBindable AggregateBalance => sampleContainer.AggregateBalance; + + public IBindable AggregateFrequency => sampleContainer.AggregateFrequency; + + public IBindable AggregateTempo => sampleContainer.AggregateTempo; + + #endregion + } +} 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..23159e4fe1 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -1,26 +1,27 @@ // 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 { + /// + /// A sound consisting of one or more samples to be played. + /// public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent { - private readonly ISampleInfo[] hitSamples; - - [Resolved] - private ISampleStore samples { get; set; } - public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; @@ -34,21 +35,74 @@ 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); - public SkinnableSound(ISampleInfo hitSamples) - : this(new[] { hitSamples }) + private readonly AudioContainer samplesContainer; + + [Resolved] + private ISampleStore sampleStore { get; set; } + + [Resolved(CanBeNull = true)] + private IPooledSampleProvider samplePool { 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 }) { } - public SkinnableSound(IEnumerable hitSamples) + private ISampleInfo[] samples = Array.Empty(); + + /// + /// The samples that should be played. + /// + public ISampleInfo[] Samples { - this.hitSamples = hitSamples.ToArray(); - InternalChild = SamplesContainer = new AudioContainer(); + get => samples; + set + { + value ??= Array.Empty(); + + if (samples == value) + return; + + samples = value; + + if (LoadState >= LoadState.Ready) + updateSamples(); + } } private bool looping; + /// + /// Whether the samples should loop on completion. + /// public bool Looping { get => looping; @@ -58,77 +112,80 @@ namespace osu.Game.Skinning looping = value; - SamplesContainer.ForEach(c => c.Looping = looping); + samplesContainer.ForEach(c => c.Looping = looping); } } + /// + /// Plays the samples. + /// public virtual void Play() { - SamplesContainer.ForEach(c => + samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) c.Play(); }); } + /// + /// Stops the samples. + /// public virtual void Stop() { - SamplesContainer.ForEach(c => c.Stop()); + samplesContainer.ForEach(c => c.Stop()); } protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateSamples(); + } + + private void updateSamples() { bool wasPlaying = IsPlaying; - var channels = hitSamples.Select(s => + // Remove all pooled samples (return them to the pool), and dispose the rest. + samplesContainer.RemoveAll(s => s.IsInPool); + samplesContainer.Clear(); + + foreach (var s in samples) { - var ch = skin.GetSample(s); + var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); + sample.Looping = Looping; + sample.Volume.Value = s.Volume / 100.0; - if (ch == null && allowFallback) - { - foreach (var lookup in s.LookupNames) - { - if ((ch = samples.Get(lookup)) != null) - break; - } - } + samplesContainer.Add(sample); + } - 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(); } #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); - public bool IsPlaying => SamplesContainer.Any(s => s.Playing); + /// + /// Whether any samples are currently 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 08811b9b8c..218f051bf0 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -37,7 +37,7 @@ namespace osu.Game.Storyboards.Drawables foreach (var mod in mods.Value.OfType()) { - foreach (var sample in SamplesContainer) + foreach (var sample in DrawableSamples) mod.ApplyToSample(sample); } } 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, 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 @@ - +