diff --git a/osu.Android.props b/osu.Android.props
index dc3e14c141..afc5d4ec52 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index f7909071ea..9e78185272 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
@@ -194,13 +193,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(0);
- AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
- // autoplay replay frames use track time;
- // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time.
- // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time,
- // as real time is what we care about for spinners
- // (so we're making the spin take 1000ms in real time *always*, regardless of the track clock's rate).
- transformReplay(replay => applyRateAdjustment(replay, rate));
+ AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate);
addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 07f40f763b..b73ae5eeb9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.BindValueChanged(updateSlidingSample);
}
- private SkinnableSound slidingSample;
+ private PausableSkinnableSound slidingSample;
protected override void LoadSamples()
{
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "sliderslide";
- AddInternal(slidingSample = new SkinnableSound(clone)
+ AddInternal(slidingSample = new PausableSkinnableSound(clone)
{
Looping = true
});
@@ -112,10 +112,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateSlidingSample(ValueChangedEvent tracking)
{
- // note that samples will not start playing if exiting a seek operation in the middle of a slider.
- // may be something we want to address at a later point, but not so easy to make happen right now
- // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
- if (tracking.NewValue && ShouldPlaySamples)
+ if (tracking.NewValue)
slidingSample?.Play();
else
slidingSample?.Stop();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index a57bb466c7..6488c60acf 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample);
}
- private SkinnableSound spinningSample;
+ private PausableSkinnableSound spinningSample;
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
@@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "spinnerspin";
- AddInternal(spinningSample = new SkinnableSound(clone)
+ AddInternal(spinningSample = new PausableSkinnableSound(clone)
{
Volume = { Value = 0 },
Looping = true,
@@ -113,10 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateSpinningSample(ValueChangedEvent tracking)
{
- // note that samples will not start playing if exiting a seek operation in the middle of a spinner.
- // may be something we want to address at a later point, but not so easy to make happen right now
- // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
- if (tracking.NewValue && ShouldPlaySamples)
+ if (tracking.NewValue)
{
spinningSample?.Play();
spinningSample?.VolumeTo(1, 200);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
index f1a782cbb5..05ed38d241 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
@@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
+using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
@@ -77,6 +79,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private bool rotationTransferred;
+ [Resolved(canBeNull: true)]
+ private GameplayClock gameplayClock { get; set; }
+
protected override void Update()
{
base.Update();
@@ -126,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
currentRotation += angle;
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
// (see: ModTimeRamp)
- RateAdjustedRotation += (float)(Math.Abs(angle) * Clock.Rate);
+ RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate));
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
index 7c39c040b1..fcf7c529f5 100644
--- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
+++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
@@ -42,9 +42,9 @@ namespace osu.Game.Rulesets.Taiko.Audio
}
}
- private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
+ private PausableSkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
{
- var drawable = new SkinnableSound(hitSampleInfo)
+ var drawable = new PausableSkinnableSound(hitSampleInfo)
{
LifetimeStart = lifetimeStart,
LifetimeEnd = lifetimeEnd
@@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Taiko.Audio
public class DrumSample
{
- public SkinnableSound Centre;
- public SkinnableSound Rim;
+ public PausableSkinnableSound Centre;
+ public PausableSkinnableSound Rim;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs
index b5ec2e8def..19493271be 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs
@@ -1,21 +1,64 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Skinning
{
public class LegacyHitExplosion : CompositeDrawable
{
- public LegacyHitExplosion(Drawable sprite)
- {
- InternalChild = sprite;
+ private readonly Drawable sprite;
+ private readonly Drawable strongSprite;
+ private DrawableStrongNestedHit nestedStrongHit;
+ private bool switchedToStrongSprite;
+
+ ///
+ /// Creates a new legacy hit explosion.
+ ///
+ ///
+ /// Contrary to stable's, this implementation doesn't require a frame-perfect hit
+ /// for the strong sprite to be displayed.
+ ///
+ /// The normal legacy explosion sprite.
+ /// The strong legacy explosion sprite.
+ public LegacyHitExplosion(Drawable sprite, Drawable strongSprite = null)
+ {
+ this.sprite = sprite;
+ this.strongSprite = strongSprite;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject judgedObject)
+ {
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
+
+ AddInternal(sprite.With(s =>
+ {
+ s.Anchor = Anchor.Centre;
+ s.Origin = Anchor.Centre;
+ }));
+
+ if (strongSprite != null)
+ {
+ AddInternal(strongSprite.With(s =>
+ {
+ s.Alpha = 0;
+ s.Anchor = Anchor.Centre;
+ s.Origin = Anchor.Centre;
+ }));
+ }
+
+ if (judgedObject is DrawableHit hit)
+ nestedStrongHit = hit.NestedHitObjects.SingleOrDefault() as DrawableStrongNestedHit;
}
protected override void LoadComplete()
@@ -33,5 +76,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning
Expire(true);
}
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (shouldSwitchToStrongSprite() && !switchedToStrongSprite)
+ {
+ sprite.FadeOut(50, Easing.OutQuint);
+ strongSprite.FadeIn(50, Easing.OutQuint);
+ switchedToStrongSprite = true;
+ }
+ }
+
+ private bool shouldSwitchToStrongSprite()
+ {
+ if (nestedStrongHit == null || strongSprite == null)
+ return false;
+
+ return nestedStrongHit.IsHit;
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index c222ccb51f..d320b824e6 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
@@ -74,15 +74,23 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null;
- case TaikoSkinComponents.TaikoExplosionGood:
- case TaikoSkinComponents.TaikoExplosionGoodStrong:
- case TaikoSkinComponents.TaikoExplosionGreat:
- case TaikoSkinComponents.TaikoExplosionGreatStrong:
case TaikoSkinComponents.TaikoExplosionMiss:
- var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
- if (sprite != null)
- return new LegacyHitExplosion(sprite);
+ var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
+ if (missSprite != null)
+ return new LegacyHitExplosion(missSprite);
+
+ return null;
+
+ case TaikoSkinComponents.TaikoExplosionGood:
+ case TaikoSkinComponents.TaikoExplosionGreat:
+
+ var hitName = getHitName(taikoComponent.Component);
+ var hitSprite = this.GetAnimation(hitName, true, false);
+ var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
+
+ if (hitSprite != null)
+ return new LegacyHitExplosion(hitSprite, strongHitSprite);
return null;
@@ -109,17 +117,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning
case TaikoSkinComponents.TaikoExplosionGood:
return "taiko-hit100";
- case TaikoSkinComponents.TaikoExplosionGoodStrong:
- return "taiko-hit100k";
-
case TaikoSkinComponents.TaikoExplosionGreat:
return "taiko-hit300";
-
- case TaikoSkinComponents.TaikoExplosionGreatStrong:
- return "taiko-hit300k";
}
- throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type");
+ throw new ArgumentOutOfRangeException(nameof(component), $"Invalid component type: {component}");
}
public override SampleChannel GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index 0d785adb4a..ac4fb51661 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -17,9 +17,7 @@ namespace osu.Game.Rulesets.Taiko
BarLine,
TaikoExplosionMiss,
TaikoExplosionGood,
- TaikoExplosionGoodStrong,
TaikoExplosionGreat,
- TaikoExplosionGreatStrong,
Scroller,
Mascot,
}
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
index efd1b25046..53765f04dd 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -10,7 +9,6 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
-using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
@@ -50,10 +48,10 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load()
{
- Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion(JudgedObject, result));
+ Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(result)), _ => new DefaultHitExplosion(JudgedObject, result));
}
- private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject)
+ private static TaikoSkinComponents getComponentName(HitResult result)
{
switch (result)
{
@@ -61,28 +59,13 @@ namespace osu.Game.Rulesets.Taiko.UI
return TaikoSkinComponents.TaikoExplosionMiss;
case HitResult.Good:
- return useStrongExplosion(judgedObject)
- ? TaikoSkinComponents.TaikoExplosionGoodStrong
- : TaikoSkinComponents.TaikoExplosionGood;
+ return TaikoSkinComponents.TaikoExplosionGood;
case HitResult.Great:
- return useStrongExplosion(judgedObject)
- ? TaikoSkinComponents.TaikoExplosionGreatStrong
- : TaikoSkinComponents.TaikoExplosionGreat;
+ return TaikoSkinComponents.TaikoExplosionGreat;
}
- throw new ArgumentOutOfRangeException(nameof(judgedObject), "Invalid result type");
- }
-
- private bool useStrongExplosion(DrawableHitObject judgedObject)
- {
- if (!(judgedObject.HitObject is Hit))
- return false;
-
- if (!(judgedObject.NestedHitObjects.SingleOrDefault() is DrawableStrongNestedHit nestedHit))
- return false;
-
- return judgedObject.Result.Type == nestedHit.Result.Type;
+ throw new ArgumentOutOfRangeException(nameof(result), $"Invalid result type: {result}");
}
///
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 7b3fbb1faf..120cf264c3 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -215,16 +215,12 @@ namespace osu.Game.Rulesets.Taiko.UI
private void addDrumRollHit(DrawableDrumRollTick drawableTick) =>
drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick));
- ///
- /// As legacy skins have different explosions for singular and double strong hits,
- /// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame.
- ///
- private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) => Schedule(() =>
+ private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type)
{
hitExplosionContainer.Add(new HitExplosion(drawableObject, result));
if (drawableObject.HitObject.Kiai)
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
- });
+ }
private class ProxyContainer : LifetimeManagementContainer
{
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index a690eb3b59..d46769a7c0 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -4,10 +4,12 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
+using osu.Framework.Graphics.Audio;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Audio;
@@ -106,9 +108,14 @@ namespace osu.Game.Tests.Gameplay
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
SelectedMods.Value = new[] { testedMod };
- Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0));
+ var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
- gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
+ Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0)
+ {
+ Child = beatmapSkinSourceContainer
+ });
+
+ beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
{
Clock = gameplayContainer.GameplayClock
});
@@ -116,7 +123,7 @@ namespace osu.Game.Tests.Gameplay
AddStep("start", () => gameplayContainer.Start());
- AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate);
+ AddAssert("sample playback rate matches mod rates", () => sample.ChildrenOfType().First().AggregateFrequency.Value == expectedRate);
}
private class TestSkin : LegacySkin
@@ -168,8 +175,6 @@ namespace osu.Game.Tests.Gameplay
: base(sampleInfo)
{
}
-
- public new SampleChannel Channel => base.Channel;
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 420bf29429..ac0e8eb0d4 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -158,7 +158,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestQuickRetryFromFailedGameplay()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
- AddStep("quick retry", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke());
+ AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke());
confirmExited();
}
@@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestQuickExitFromFailedGameplay()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
- AddStep("quick exit", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke());
+ AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke());
confirmExited();
}
@@ -183,7 +183,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestQuickExitFromGameplay()
{
- AddStep("quick exit", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke());
+ AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke());
confirmExited();
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index ed75d83151..8f2011e5dd 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -22,11 +22,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableSound : OsuTestScene
{
- [Cached]
+ [Cached(typeof(ISamplePlaybackDisabler))]
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
private TestSkinSourceContainer skinSource;
- private SkinnableSound skinnableSound;
+ private PausableSkinnableSound skinnableSound;
[SetUp]
public void SetUp() => Schedule(() =>
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Clock = gameplayClock,
RelativeSizeAxes = Axes.Both,
- Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide"))
+ Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
},
};
});
diff --git a/osu.Game/Rulesets/Mods/IApplicableToSample.cs b/osu.Game/Rulesets/Mods/IApplicableToSample.cs
index 559d127cfc..50a6d501b6 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToSample.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToSample.cs
@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Audio.Sample;
+using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods
{
@@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods
///
public interface IApplicableToSample : IApplicableMod
{
- void ApplyToSample(SampleChannel sample);
+ void ApplyToSample(DrawableSample sample);
}
}
diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs
index 4004953cd1..282de3a8e1 100644
--- a/osu.Game/Rulesets/Mods/ModNightcore.cs
+++ b/osu.Game/Rulesets/Mods/ModNightcore.cs
@@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mods
public class NightcoreBeatContainer : BeatSyncedContainer
{
- private SkinnableSound hatSample;
- private SkinnableSound clapSample;
- private SkinnableSound kickSample;
- private SkinnableSound finishSample;
+ private PausableSkinnableSound hatSample;
+ private PausableSkinnableSound clapSample;
+ private PausableSkinnableSound kickSample;
+ private PausableSkinnableSound finishSample;
private int? firstBeat;
@@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Mods
{
InternalChildren = new Drawable[]
{
- hatSample = new SkinnableSound(new SampleInfo("nightcore-hat")),
- clapSample = new SkinnableSound(new SampleInfo("nightcore-clap")),
- kickSample = new SkinnableSound(new SampleInfo("nightcore-kick")),
- finishSample = new SkinnableSound(new SampleInfo("nightcore-finish")),
+ hatSample = new PausableSkinnableSound(new SampleInfo("nightcore-hat")),
+ clapSample = new PausableSkinnableSound(new SampleInfo("nightcore-clap")),
+ kickSample = new PausableSkinnableSound(new SampleInfo("nightcore-kick")),
+ finishSample = new PausableSkinnableSound(new SampleInfo("nightcore-finish")),
};
}
diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs
index fec21764b0..2150b0fb68 100644
--- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
-using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
+using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods
{
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
}
- public virtual void ApplyToSample(SampleChannel sample)
+ public virtual void ApplyToSample(DrawableSample sample)
{
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
index 20c8d0f3e7..4d43ae73d3 100644
--- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs
+++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
@@ -6,11 +6,11 @@ using System.Linq;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
+using osu.Framework.Graphics.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
-using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Objects;
-using osu.Framework.Audio.Sample;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods
{
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.TriggerChange();
}
- public void ApplyToSample(SampleChannel sample)
+ public void ApplyToSample(DrawableSample sample)
{
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 7c05bc9aa7..eb12c1cdfc 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -17,7 +17,6 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Configuration;
-using osu.Game.Screens.Play;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Drawables
@@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
public readonly Bindable AccentColour = new Bindable(Color4.Gray);
- protected SkinnableSound Samples { get; private set; }
+ protected PausableSkinnableSound Samples { get; private set; }
public virtual IEnumerable GetSamples() => HitObject.Samples;
@@ -179,7 +178,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
- Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)));
+ Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)));
AddInternal(Samples);
}
@@ -359,9 +358,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
}
- [Resolved(canBeNull: true)]
- private GameplayClock gameplayClock { get; set; }
-
///
/// Calculate the position to be used for sample playback at a specified X position (0..1).
///
@@ -374,18 +370,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0);
}
- ///
- /// Whether samples should currently be playing. Will be false during seek operations.
- ///
- protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true;
-
///
/// Plays all the hit sounds for this .
/// This is invoked automatically when this is hit.
///
public virtual void PlaySamples()
{
- if (Samples != null && ShouldPlaySamples)
+ if (Samples != null)
{
Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition);
Samples.Play();
diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index a4af92749f..55c4edfbd1 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -2,7 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
@@ -59,7 +62,7 @@ namespace osu.Game.Rulesets.UI
{
if (clock != null)
{
- stabilityGameplayClock.ParentGameplayClock = parentGameplayClock = clock;
+ parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock;
GameplayClock.IsPaused.BindTo(clock.IsPaused);
}
}
@@ -215,7 +218,9 @@ namespace osu.Game.Rulesets.UI
private class StabilityGameplayClock : GameplayClock
{
- public IFrameBasedClock ParentGameplayClock;
+ public GameplayClock ParentGameplayClock;
+
+ public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>();
public StabilityGameplayClock(FramedClock underlyingClock)
: base(underlyingClock)
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs
index 102955657e..8c0e31c04c 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs
@@ -1,70 +1,50 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Specialized;
using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics;
-using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
///
/// The part of the timeline that displays the control points.
///
- public class ControlPointPart : TimelinePart
+ public class ControlPointPart : TimelinePart
{
+ private IBindableList controlPointGroups;
+
protected override void LoadBeatmap(WorkingBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
- ControlPointInfo cpi = beatmap.Beatmap.ControlPointInfo;
-
- cpi.TimingPoints.ForEach(addTimingPoint);
-
- // Consider all non-timing points as the same type
- cpi.SamplePoints.Select(c => (ControlPoint)c)
- .Concat(cpi.EffectPoints)
- .Concat(cpi.DifficultyPoints)
- .Distinct()
- // Non-timing points should not be added where there are timing points
- .Where(c => cpi.TimingPointAt(c.Time).Time != c.Time)
- .ForEach(addNonTimingPoint);
- }
-
- private void addTimingPoint(ControlPoint controlPoint) => Add(new TimingPointVisualisation(controlPoint));
- private void addNonTimingPoint(ControlPoint controlPoint) => Add(new NonTimingPointVisualisation(controlPoint));
-
- private class TimingPointVisualisation : ControlPointVisualisation
- {
- public TimingPointVisualisation(ControlPoint controlPoint)
- : base(controlPoint)
+ controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
+ controlPointGroups.BindCollectionChanged((sender, args) =>
{
- }
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Reset:
+ Clear();
+ break;
- [BackgroundDependencyLoader]
- private void load(OsuColour colours) => Colour = colours.YellowDark;
- }
+ case NotifyCollectionChangedAction.Add:
+ foreach (var group in args.NewItems.OfType())
+ Add(new GroupVisualisation(group));
+ break;
- private class NonTimingPointVisualisation : ControlPointVisualisation
- {
- public NonTimingPointVisualisation(ControlPoint controlPoint)
- : base(controlPoint)
- {
- }
+ case NotifyCollectionChangedAction.Remove:
+ foreach (var group in args.OldItems.OfType())
+ {
+ var matching = Children.SingleOrDefault(gv => gv.Group == group);
- [BackgroundDependencyLoader]
- private void load(OsuColour colours) => Colour = colours.Green;
- }
+ matching?.Expire();
+ }
- private abstract class ControlPointVisualisation : PointVisualisation
- {
- protected ControlPointVisualisation(ControlPoint controlPoint)
- : base(controlPoint.Time)
- {
- }
+ break;
+ }
+ }, true);
}
}
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs
new file mode 100644
index 0000000000..b9eb53b697
--- /dev/null
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs
@@ -0,0 +1,46 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
+{
+ public class GroupVisualisation : PointVisualisation
+ {
+ public readonly ControlPointGroup Group;
+
+ private BindableList controlPoints;
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ public GroupVisualisation(ControlPointGroup group)
+ : base(group.Time)
+ {
+ Group = group;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy();
+ controlPoints.BindCollectionChanged((_, __) =>
+ {
+ if (controlPoints.Count == 0)
+ {
+ Colour = Color4.Transparent;
+ return;
+ }
+
+ Colour = controlPoints.Any(c => c is TimingControlPoint) ? colours.YellowDark : colours.Green;
+ }, true);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index b95b3842b3..bc2ccfc605 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -3,18 +3,23 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
@@ -34,11 +39,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly List shadowComponents = new List();
+ private DrawableHitObject drawableHitObject;
+
+ private Bindable comboColour;
+
+ private readonly Container mainComponents;
+
+ private readonly OsuSpriteText comboIndexText;
+
+ private Bindable comboIndex;
+
private const float thickness = 5;
private const float shadow_radius = 5;
- private const float circle_size = 16;
+ private const float circle_size = 24;
public TimelineHitObjectBlueprint(HitObject hitObject)
: base(hitObject)
@@ -54,14 +69,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
+ AddRangeInternal(new Drawable[]
+ {
+ mainComponents = new Container
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ comboIndexText = new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Numeric.With(size: circle_size / 2, weight: FontWeight.Black),
+ },
+ });
+
circle = new Circle
{
Size = new Vector2(circle_size),
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
- RelativePositionAxes = Axes.X,
- AlwaysPresent = true,
- Colour = Color4.White,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
@@ -77,7 +106,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
DragBar dragBarUnderlay;
Container extensionBar;
- AddRangeInternal(new Drawable[]
+ mainComponents.AddRange(new Drawable[]
{
extensionBar = new Container
{
@@ -117,12 +146,54 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
else
{
- AddInternal(circle);
+ mainComponents.Add(circle);
}
updateShadows();
}
+ [BackgroundDependencyLoader(true)]
+ private void load(HitObjectComposer composer)
+ {
+ if (composer != null)
+ {
+ // best effort to get the drawable representation for grabbing colour and what not.
+ drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject);
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (HitObject is IHasComboInformation comboInfo)
+ {
+ comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
+ comboIndex.BindValueChanged(combo =>
+ {
+ comboIndexText.Text = (combo.NewValue + 1).ToString();
+ }, true);
+ }
+
+ if (drawableHitObject != null)
+ {
+ comboColour = drawableHitObject.AccentColour.GetBoundCopy();
+ comboColour.BindValueChanged(colour =>
+ {
+ if (HitObject is IHasDuration)
+ mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White);
+ else
+ mainComponents.Colour = drawableHitObject.AccentColour.Value;
+
+ var col = mainComponents.Colour.TopLeft.Linear;
+ float brightness = col.R + col.G + col.B;
+
+ // decide the combo index colour based on brightness?
+ comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White;
+ }, true);
+ }
+ }
+
protected override void Update()
{
base.Update();
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index d7a4661fa0..5282b4d998 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -1,8 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Skinning;
@@ -18,11 +22,23 @@ namespace osu.Game.Screens.Edit.Compose
{
}
- protected override Drawable CreateMainContent()
+ private Ruleset ruleset;
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
- var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance();
+ var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
+ ruleset = parent.Get>().Value.BeatmapInfo.Ruleset?.CreateInstance();
composer = ruleset?.CreateHitObjectComposer();
+ // make the composer available to the timeline and other components in this screen.
+ dependencies.CacheAs(composer);
+
+ return dependencies;
+ }
+
+ protected override Drawable CreateMainContent()
+ {
if (ruleset == null || composer == null)
return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer");
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index fd090e0959..a0692d94e6 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -107,6 +107,7 @@ namespace osu.Game.Screens.Edit
UpdateClockSource();
dependencies.CacheAs(clock);
+ dependencies.CacheAs(clock);
AddInternal(clock);
// todo: remove caching of this and consume via editorBeatmap?
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index ec203df064..4b7cd82637 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -7,17 +7,18 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
-using osu.Framework.Utils;
using osu.Framework.Timing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Screens.Play;
namespace osu.Game.Screens.Edit
{
///
/// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
///
- public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
+ public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISamplePlaybackDisabler
{
public IBindable