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 @@
-
+