1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-06 08:22:56 +08:00

Merge branch 'master' into scoring-standardisation

This commit is contained in:
smoogipoo 2020-10-01 12:28:51 +09:00
commit cb6b35116a
46 changed files with 663 additions and 319 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.925.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.930.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -194,13 +193,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(0); addSeekStep(0);
AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate))); AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = 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));
addSeekStep(1000); addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));

View File

@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.BindValueChanged(updateSlidingSample); Tracking.BindValueChanged(updateSlidingSample);
} }
private SkinnableSound slidingSample; private PausableSkinnableSound slidingSample;
protected override void LoadSamples() protected override void LoadSamples()
{ {
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "sliderslide"; clone.Name = "sliderslide";
AddInternal(slidingSample = new SkinnableSound(clone) AddInternal(slidingSample = new PausableSkinnableSound(clone)
{ {
Looping = true Looping = true
}); });
@ -112,10 +112,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateSlidingSample(ValueChangedEvent<bool> tracking) private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{ {
// note that samples will not start playing if exiting a seek operation in the middle of a slider. if (tracking.NewValue)
// 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)
slidingSample?.Play(); slidingSample?.Play();
else else
slidingSample?.Stop(); slidingSample?.Stop();

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample); isSpinning.BindValueChanged(updateSpinningSample);
} }
private SkinnableSound spinningSample; private PausableSkinnableSound spinningSample;
private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f; 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); var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "spinnerspin"; clone.Name = "spinnerspin";
AddInternal(spinningSample = new SkinnableSound(clone) AddInternal(spinningSample = new PausableSkinnableSound(clone)
{ {
Volume = { Value = 0 }, Volume = { Value = 0 },
Looping = true, Looping = true,
@ -113,10 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateSpinningSample(ValueChangedEvent<bool> tracking) private void updateSpinningSample(ValueChangedEvent<bool> tracking)
{ {
// note that samples will not start playing if exiting a seek operation in the middle of a spinner. if (tracking.NewValue)
// 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)
{ {
spinningSample?.Play(); spinningSample?.Play();
spinningSample?.VolumeTo(1, 200); spinningSample?.VolumeTo(1, 200);

View File

@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Screens.Play;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
@ -77,6 +79,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private bool rotationTransferred; private bool rotationTransferred;
[Resolved(canBeNull: true)]
private GameplayClock gameplayClock { get; set; }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -126,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
currentRotation += angle; currentRotation += angle;
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
// (see: ModTimeRamp) // (see: ModTimeRamp)
RateAdjustedRotation += (float)(Math.Abs(angle) * Clock.Rate); RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate));
} }
} }
} }

View File

@ -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, LifetimeStart = lifetimeStart,
LifetimeEnd = lifetimeEnd LifetimeEnd = lifetimeEnd
@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Taiko.Audio
public class DrumSample public class DrumSample
{ {
public SkinnableSound Centre; public PausableSkinnableSound Centre;
public SkinnableSound Rim; public PausableSkinnableSound Rim;
} }
} }
} }

View File

@ -1,21 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // 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;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Skinning namespace osu.Game.Rulesets.Taiko.Skinning
{ {
public class LegacyHitExplosion : CompositeDrawable public class LegacyHitExplosion : CompositeDrawable
{ {
public LegacyHitExplosion(Drawable sprite) private readonly Drawable sprite;
{ private readonly Drawable strongSprite;
InternalChild = sprite;
private DrawableStrongNestedHit nestedStrongHit;
private bool switchedToStrongSprite;
/// <summary>
/// Creates a new legacy hit explosion.
/// </summary>
/// <remarks>
/// Contrary to stable's, this implementation doesn't require a frame-perfect hit
/// for the strong sprite to be displayed.
/// </remarks>
/// <param name="sprite">The normal legacy explosion sprite.</param>
/// <param name="strongSprite">The strong legacy explosion sprite.</param>
public LegacyHitExplosion(Drawable sprite, Drawable strongSprite = null)
{
this.sprite = sprite;
this.strongSprite = strongSprite;
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject judgedObject)
{
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; 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() protected override void LoadComplete()
@ -33,5 +76,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning
Expire(true); 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;
}
} }
} }

View File

@ -74,15 +74,23 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null; return null;
case TaikoSkinComponents.TaikoExplosionGood:
case TaikoSkinComponents.TaikoExplosionGoodStrong:
case TaikoSkinComponents.TaikoExplosionGreat:
case TaikoSkinComponents.TaikoExplosionGreatStrong:
case TaikoSkinComponents.TaikoExplosionMiss: case TaikoSkinComponents.TaikoExplosionMiss:
var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false); var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
if (sprite != null) if (missSprite != null)
return new LegacyHitExplosion(sprite); 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; return null;
@ -109,17 +117,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning
case TaikoSkinComponents.TaikoExplosionGood: case TaikoSkinComponents.TaikoExplosionGood:
return "taiko-hit100"; return "taiko-hit100";
case TaikoSkinComponents.TaikoExplosionGoodStrong:
return "taiko-hit100k";
case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionGreat:
return "taiko-hit300"; 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)); public override SampleChannel GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));

View File

@ -17,9 +17,7 @@ namespace osu.Game.Rulesets.Taiko
BarLine, BarLine,
TaikoExplosionMiss, TaikoExplosionMiss,
TaikoExplosionGood, TaikoExplosionGood,
TaikoExplosionGoodStrong,
TaikoExplosionGreat, TaikoExplosionGreat,
TaikoExplosionGreatStrong,
Scroller, Scroller,
Mascot, Mascot,
} }

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osuTK; using osuTK;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,7 +9,6 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI namespace osu.Game.Rulesets.Taiko.UI
@ -50,10 +48,10 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() 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) switch (result)
{ {
@ -61,28 +59,13 @@ namespace osu.Game.Rulesets.Taiko.UI
return TaikoSkinComponents.TaikoExplosionMiss; return TaikoSkinComponents.TaikoExplosionMiss;
case HitResult.Good: case HitResult.Good:
return useStrongExplosion(judgedObject) return TaikoSkinComponents.TaikoExplosionGood;
? TaikoSkinComponents.TaikoExplosionGoodStrong
: TaikoSkinComponents.TaikoExplosionGood;
case HitResult.Great: case HitResult.Great:
return useStrongExplosion(judgedObject) return TaikoSkinComponents.TaikoExplosionGreat;
? TaikoSkinComponents.TaikoExplosionGreatStrong
: TaikoSkinComponents.TaikoExplosionGreat;
} }
throw new ArgumentOutOfRangeException(nameof(judgedObject), "Invalid result type"); throw new ArgumentOutOfRangeException(nameof(result), $"Invalid result type: {result}");
}
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;
} }
/// <summary> /// <summary>

View File

@ -215,16 +215,12 @@ namespace osu.Game.Rulesets.Taiko.UI
private void addDrumRollHit(DrawableDrumRollTick drawableTick) => private void addDrumRollHit(DrawableDrumRollTick drawableTick) =>
drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick)); drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick));
/// <remarks> private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type)
/// 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.
/// </remarks>
private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) => Schedule(() =>
{ {
hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); hitExplosionContainer.Add(new HitExplosion(drawableObject, result));
if (drawableObject.HitObject.Kiai) if (drawableObject.HitObject.Kiai)
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
}); }
private class ProxyContainer : LifetimeManagementContainer private class ProxyContainer : LifetimeManagementContainer
{ {

View File

@ -4,10 +4,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Graphics.Audio;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
@ -106,9 +108,14 @@ namespace osu.Game.Tests.Gameplay
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
SelectedMods.Value = new[] { testedMod }; 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 Clock = gameplayContainer.GameplayClock
}); });
@ -116,7 +123,7 @@ namespace osu.Game.Tests.Gameplay
AddStep("start", () => gameplayContainer.Start()); 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<DrawableSample>().First().AggregateFrequency.Value == expectedRate);
} }
private class TestSkin : LegacySkin private class TestSkin : LegacySkin
@ -168,8 +175,6 @@ namespace osu.Game.Tests.Gameplay
: base(sampleInfo) : base(sampleInfo)
{ {
} }
public new SampleChannel Channel => base.Channel;
} }
} }
} }

View File

@ -4,7 +4,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
@ -17,16 +17,26 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(IBeatSnapProvider))] [Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap; private readonly EditorBeatmap editorBeatmap;
protected override bool ScrollUsingMouseWheel => false;
public TestSceneTimingScreen() public TestSceneTimingScreen()
{ {
editorBeatmap = new EditorBeatmap(new OsuBeatmap()); editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Beatmap.Disabled = true;
Child = new TimingScreen(); Child = new TimingScreen();
} }
protected override void Dispose(bool isDisposing)
{
Beatmap.Disabled = false;
base.Dispose(isDisposing);
}
} }
} }

View File

@ -158,7 +158,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestQuickRetryFromFailedGameplay() public void TestQuickRetryFromFailedGameplay()
{ {
AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("quick retry", () => Player.GameplayClockContainer.OfType<HotkeyRetryOverlay>().First().Action?.Invoke()); AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType<HotkeyRetryOverlay>().First().Action?.Invoke());
confirmExited(); confirmExited();
} }
@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestQuickExitFromFailedGameplay() public void TestQuickExitFromFailedGameplay()
{ {
AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("quick exit", () => Player.GameplayClockContainer.OfType<HotkeyExitOverlay>().First().Action?.Invoke()); AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType<HotkeyExitOverlay>().First().Action?.Invoke());
confirmExited(); confirmExited();
} }
@ -183,7 +183,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestQuickExitFromGameplay() public void TestQuickExitFromGameplay()
{ {
AddStep("quick exit", () => Player.GameplayClockContainer.OfType<HotkeyExitOverlay>().First().Action?.Invoke()); AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType<HotkeyExitOverlay>().First().Action?.Invoke());
confirmExited(); confirmExited();
} }

View File

@ -22,11 +22,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneSkinnableSound : OsuTestScene public class TestSceneSkinnableSound : OsuTestScene
{ {
[Cached] [Cached(typeof(ISamplePlaybackDisabler))]
private GameplayClock gameplayClock = new GameplayClock(new FramedClock()); private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
private TestSkinSourceContainer skinSource; private TestSkinSourceContainer skinSource;
private SkinnableSound skinnableSound; private PausableSkinnableSound skinnableSound;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Clock = gameplayClock, Clock = gameplayClock,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide")) Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
}, },
}; };
}); });

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Game.Extensions
{
public static class EditorDisplayExtensions
{
/// <summary>
/// Get an editor formatted string (mm:ss:mss)
/// </summary>
/// <param name="milliseconds">A time value in milliseconds.</param>
/// <returns>An editor formatted display string.</returns>
public static string ToEditorFormattedString(this double milliseconds) =>
ToEditorFormattedString(TimeSpan.FromMilliseconds(milliseconds));
/// <summary>
/// Get an editor formatted string (mm:ss:mss)
/// </summary>
/// <param name="timeSpan">A time value.</param>
/// <returns>An editor formatted display string.</returns>
public static string ToEditorFormattedString(this TimeSpan timeSpan) =>
$"{(timeSpan < TimeSpan.Zero ? "-" : string.Empty)}{timeSpan:mm\\:ss\\:fff}";
}
}

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // 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 namespace osu.Game.Rulesets.Mods
{ {
@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
public interface IApplicableToSample : IApplicableMod public interface IApplicableToSample : IApplicableMod
{ {
void ApplyToSample(SampleChannel sample); void ApplyToSample(DrawableSample sample);
} }
} }

View File

@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mods
public class NightcoreBeatContainer : BeatSyncedContainer public class NightcoreBeatContainer : BeatSyncedContainer
{ {
private SkinnableSound hatSample; private PausableSkinnableSound hatSample;
private SkinnableSound clapSample; private PausableSkinnableSound clapSample;
private SkinnableSound kickSample; private PausableSkinnableSound kickSample;
private SkinnableSound finishSample; private PausableSkinnableSound finishSample;
private int? firstBeat; private int? firstBeat;
@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Mods
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
hatSample = new SkinnableSound(new SampleInfo("nightcore-hat")), hatSample = new PausableSkinnableSound(new SampleInfo("nightcore-hat")),
clapSample = new SkinnableSound(new SampleInfo("nightcore-clap")), clapSample = new PausableSkinnableSound(new SampleInfo("nightcore-clap")),
kickSample = new SkinnableSound(new SampleInfo("nightcore-kick")), kickSample = new PausableSkinnableSound(new SampleInfo("nightcore-kick")),
finishSample = new SkinnableSound(new SampleInfo("nightcore-finish")), finishSample = new PausableSkinnableSound(new SampleInfo("nightcore-finish")),
}; };
} }

View File

@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
} }
public virtual void ApplyToSample(SampleChannel sample) public virtual void ApplyToSample(DrawableSample sample)
{ {
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
} }

View File

@ -6,11 +6,11 @@ using System.Linq;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Framework.Audio.Sample; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.TriggerChange(); AdjustPitch.TriggerChange();
} }
public void ApplyToSample(SampleChannel sample) public void ApplyToSample(DrawableSample sample)
{ {
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
} }

View File

@ -18,7 +18,6 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Screens.Play;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Drawables namespace osu.Game.Rulesets.Objects.Drawables
@ -35,7 +34,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary> /// </summary>
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(Color4.Gray); public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(Color4.Gray);
protected SkinnableSound Samples { get; private set; } protected PausableSkinnableSound Samples { get; private set; }
public virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples; public virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples;
@ -180,7 +179,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); + $" 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); AddInternal(Samples);
} }
@ -360,9 +359,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
{ {
} }
[Resolved(canBeNull: true)]
private GameplayClock gameplayClock { get; set; }
/// <summary> /// <summary>
/// Calculate the position to be used for sample playback at a specified X position (0..1). /// Calculate the position to be used for sample playback at a specified X position (0..1).
/// </summary> /// </summary>
@ -375,18 +371,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0); return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0);
} }
/// <summary>
/// Whether samples should currently be playing. Will be false during seek operations.
/// </summary>
protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true;
/// <summary> /// <summary>
/// Plays all the hit sounds for this <see cref="DrawableHitObject"/>. /// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
/// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit. /// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
/// </summary> /// </summary>
public virtual void PlaySamples() public virtual void PlaySamples()
{ {
if (Samples != null && ShouldPlaySamples) if (Samples != null)
{ {
Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition); Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition);
Samples.Play(); Samples.Play();

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
[Description(@"")] [Description(@"")]
[Order(14)] [Order(14)]
None = 0, None,
/// <summary> /// <summary>
/// Indicates that the object has been judged as a miss. /// Indicates that the object has been judged as a miss.
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Scoring
/// </remarks> /// </remarks>
[Description(@"Miss")] [Description(@"Miss")]
[Order(5)] [Order(5)]
Miss = 64, Miss,
[Description(@"Meh")] [Description(@"Meh")]
[Order(4)] [Order(4)]
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates small tick miss. /// Indicates small tick miss.
/// </summary> /// </summary>
[Order(11)] [Order(11)]
SmallTickMiss = 128, SmallTickMiss,
/// <summary> /// <summary>
/// Indicates a small tick hit. /// Indicates a small tick hit.
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a large tick miss. /// Indicates a large tick miss.
/// </summary> /// </summary>
[Order(10)] [Order(10)]
LargeTickMiss = 192, LargeTickMiss,
/// <summary> /// <summary>
/// Indicates a large tick hit. /// Indicates a large tick hit.
@ -85,20 +85,20 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
[Description("S Bonus")] [Description("S Bonus")]
[Order(9)] [Order(9)]
SmallBonus = 254, SmallBonus,
/// <summary> /// <summary>
/// Indicates a large bonus. /// Indicates a large bonus.
/// </summary> /// </summary>
[Description("L Bonus")] [Description("L Bonus")]
[Order(8)] [Order(8)]
LargeBonus = 320, LargeBonus,
/// <summary> /// <summary>
/// Indicates a miss that should be ignored for scoring purposes. /// Indicates a miss that should be ignored for scoring purposes.
/// </summary> /// </summary>
[Order(13)] [Order(13)]
IgnoreMiss = 384, IgnoreMiss,
/// <summary> /// <summary>
/// Indicates a hit that should be ignored for scoring purposes. /// Indicates a hit that should be ignored for scoring purposes.

View File

@ -2,7 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -59,7 +62,7 @@ namespace osu.Game.Rulesets.UI
{ {
if (clock != null) if (clock != null)
{ {
stabilityGameplayClock.ParentGameplayClock = parentGameplayClock = clock; parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock;
GameplayClock.IsPaused.BindTo(clock.IsPaused); GameplayClock.IsPaused.BindTo(clock.IsPaused);
} }
} }
@ -215,7 +218,9 @@ namespace osu.Game.Rulesets.UI
private class StabilityGameplayClock : GameplayClock private class StabilityGameplayClock : GameplayClock
{ {
public IFrameBasedClock ParentGameplayClock; public GameplayClock ParentGameplayClock;
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<Bindable<double>>();
public StabilityGameplayClock(FramedClock underlyingClock) public StabilityGameplayClock(FramedClock underlyingClock)
: base(underlyingClock) : base(underlyingClock)

View File

@ -3,8 +3,8 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Components namespace osu.Game.Screens.Edit.Components
@ -35,9 +35,7 @@ namespace osu.Game.Screens.Edit.Components
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString();
var timespan = TimeSpan.FromMilliseconds(editorClock.CurrentTime);
trackTimer.Text = $"{(timespan < TimeSpan.Zero ? "-" : string.Empty)}{timespan:mm\\:ss\\:fff}";
} }
} }
} }

View File

@ -1,70 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; 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 namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
/// <summary> /// <summary>
/// The part of the timeline that displays the control points. /// The part of the timeline that displays the control points.
/// </summary> /// </summary>
public class ControlPointPart : TimelinePart public class ControlPointPart : TimelinePart<GroupVisualisation>
{ {
private IBindableList<ControlPointGroup> controlPointGroups;
protected override void LoadBeatmap(WorkingBeatmap beatmap) protected override void LoadBeatmap(WorkingBeatmap beatmap)
{ {
base.LoadBeatmap(beatmap); base.LoadBeatmap(beatmap);
ControlPointInfo cpi = beatmap.Beatmap.ControlPointInfo; controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
controlPointGroups.BindCollectionChanged((sender, args) =>
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)
{ {
} switch (args.Action)
{
case NotifyCollectionChangedAction.Reset:
Clear();
break;
[BackgroundDependencyLoader] case NotifyCollectionChangedAction.Add:
private void load(OsuColour colours) => Colour = colours.YellowDark; foreach (var group in args.NewItems.OfType<ControlPointGroup>())
} Add(new GroupVisualisation(group));
break;
private class NonTimingPointVisualisation : ControlPointVisualisation case NotifyCollectionChangedAction.Remove:
{ foreach (var group in args.OldItems.OfType<ControlPointGroup>())
public NonTimingPointVisualisation(ControlPoint controlPoint) {
: base(controlPoint) var matching = Children.SingleOrDefault(gv => gv.Group == group);
{
}
[BackgroundDependencyLoader] matching?.Expire();
private void load(OsuColour colours) => Colour = colours.Green; }
}
private abstract class ControlPointVisualisation : PointVisualisation break;
{ }
protected ControlPointVisualisation(ControlPoint controlPoint) }, true);
: base(controlPoint.Time)
{
}
} }
} }
} }

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ControlPoint> controlPoints;
[Resolved]
private OsuColour colours { get; set; }
public GroupVisualisation(ControlPointGroup group)
: base(group.Time)
{
Group = group;
}
protected override void LoadComplete()
{
base.LoadComplete();
controlPoints = (BindableList<ControlPoint>)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);
}
}
}

View File

@ -14,9 +14,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class TimelineArea : Container public class TimelineArea : Container
{ {
private readonly Timeline timeline = new Timeline { RelativeSizeAxes = Axes.Both }; public readonly Timeline Timeline = new Timeline { RelativeSizeAxes = Axes.Both };
protected override Container<Drawable> Content => timeline; protected override Container<Drawable> Content => Timeline;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
} }
}, },
timeline Timeline
}, },
}, },
ColumnDimensions = new[] ColumnDimensions = new[]
@ -121,9 +121,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
waveformCheckbox.Current.Value = true; waveformCheckbox.Current.Value = true;
timeline.WaveformVisible.BindTo(waveformCheckbox.Current); Timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
} }
private void changeZoom(float change) => timeline.Zoom += change; private void changeZoom(float change) => Timeline.Zoom += change;
} }
} }

View File

@ -3,18 +3,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -34,11 +39,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly List<Container> shadowComponents = new List<Container>(); private readonly List<Container> shadowComponents = new List<Container>();
private DrawableHitObject drawableHitObject;
private Bindable<Color4> comboColour;
private readonly Container mainComponents;
private readonly OsuSpriteText comboIndexText;
private Bindable<int> comboIndex;
private const float thickness = 5; private const float thickness = 5;
private const float shadow_radius = 5; private const float shadow_radius = 5;
private const float circle_size = 16; private const float circle_size = 24;
public TimelineHitObjectBlueprint(HitObject hitObject) public TimelineHitObjectBlueprint(HitObject hitObject)
: base(hitObject) : base(hitObject)
@ -54,14 +69,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; 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 circle = new Circle
{ {
Size = new Vector2(circle_size), Size = new Vector2(circle_size),
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
AlwaysPresent = true,
Colour = Color4.White,
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Shadow, Type = EdgeEffectType.Shadow,
@ -77,7 +106,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
DragBar dragBarUnderlay; DragBar dragBarUnderlay;
Container extensionBar; Container extensionBar;
AddRangeInternal(new Drawable[] mainComponents.AddRange(new Drawable[]
{ {
extensionBar = new Container extensionBar = new Container
{ {
@ -117,12 +146,54 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
else else
{ {
AddInternal(circle); mainComponents.Add(circle);
} }
updateShadows(); 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() protected override void Update()
{ {
base.Update(); base.Update();

View File

@ -1,8 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // 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;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Skinning; 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<IBindable<WorkingBeatmap>>().Value.BeatmapInfo.Ruleset?.CreateInstance();
composer = ruleset?.CreateHitObjectComposer(); 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) if (ruleset == null || composer == null)
return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer");

View File

@ -107,6 +107,7 @@ namespace osu.Game.Screens.Edit
UpdateClockSource(); UpdateClockSource();
dependencies.CacheAs(clock); dependencies.CacheAs(clock);
dependencies.CacheAs<ISamplePlaybackDisabler>(clock);
AddInternal(clock); AddInternal(clock);
// todo: remove caching of this and consume via editorBeatmap? // todo: remove caching of this and consume via editorBeatmap?

View File

@ -7,17 +7,18 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.Transforms;
using osu.Framework.Utils;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
{ {
/// <summary> /// <summary>
/// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor. /// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
/// </summary> /// </summary>
public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISamplePlaybackDisabler
{ {
public IBindable<Track> Track => track; public IBindable<Track> Track => track;
@ -31,6 +32,10 @@ namespace osu.Game.Screens.Edit
private readonly DecoupleableInterpolatingFramedClock underlyingClock; private readonly DecoupleableInterpolatingFramedClock underlyingClock;
public IBindable<bool> SamplePlaybackDisabled => samplePlaybackDisabled;
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor)
: this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor) : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor)
{ {
@ -166,11 +171,14 @@ namespace osu.Game.Screens.Edit
public void Stop() public void Stop()
{ {
samplePlaybackDisabled.Value = true;
underlyingClock.Stop(); underlyingClock.Stop();
} }
public bool Seek(double position) public bool Seek(double position)
{ {
samplePlaybackDisabled.Value = true;
ClearTransforms(); ClearTransforms();
return underlyingClock.Seek(position); return underlyingClock.Seek(position);
} }
@ -211,8 +219,35 @@ namespace osu.Game.Screens.Edit
private const double transform_time = 300; private const double transform_time = 300;
protected override void Update()
{
base.Update();
updateSeekingState();
}
private void updateSeekingState()
{
if (samplePlaybackDisabled.Value)
{
if (track.Value?.IsRunning != true)
{
// seeking in the editor can happen while the track isn't running.
// in this case we always want to expose ourselves as seeking (to avoid sample playback).
return;
}
// we are either running a seek tween or doing an immediate seek.
// in the case of an immediate seek the seeking bool will be set to false after one update.
// this allows for silencing hit sounds and the likes.
samplePlaybackDisabled.Value = Transforms.Any();
}
}
public void SeekTo(double seekDestination) public void SeekTo(double seekDestination)
{ {
samplePlaybackDisabled.Value = true;
if (IsRunning) if (IsRunning)
Seek(seekDestination); Seek(seekDestination);
else else

View File

@ -115,10 +115,18 @@ namespace osu.Game.Screens.Edit
new TimelineTickDisplay(), new TimelineTickDisplay(),
CreateTimelineContent(), CreateTimelineContent(),
} }
}, timelineContainer.Add); }, t =>
{
timelineContainer.Add(t);
OnTimelineLoaded(t);
});
}); });
} }
protected virtual void OnTimelineLoaded(TimelineArea timelineArea)
{
}
protected abstract Drawable CreateMainContent(); protected abstract Drawable CreateMainContent();
protected virtual Drawable CreateTimelineContent() => new Container(); protected virtual Drawable CreateTimelineContent() => new Container();

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -89,7 +90,7 @@ namespace osu.Game.Screens.Edit.Timing
}, },
new OsuSpriteText new OsuSpriteText
{ {
Text = $"{group.Time:n0}ms", Text = group.Time.ToEditorFormattedString(),
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold) Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold)
}, },
new ControlGroupAttributes(group), new ControlGroupAttributes(group),

View File

@ -12,6 +12,8 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK; using osuTK;
namespace osu.Game.Screens.Edit.Timing namespace osu.Game.Screens.Edit.Timing
@ -29,6 +31,11 @@ namespace osu.Game.Screens.Edit.Timing
{ {
} }
protected override Drawable CreateTimelineContent() => new ControlPointPart
{
RelativeSizeAxes = Axes.Both,
};
protected override Drawable CreateMainContent() => new GridContainer protected override Drawable CreateMainContent() => new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -58,6 +65,12 @@ namespace osu.Game.Screens.Edit.Timing
}); });
} }
protected override void OnTimelineLoaded(TimelineArea timelineArea)
{
base.OnTimelineLoaded(timelineArea);
timelineArea.Timeline.Zoom = timelineArea.Timeline.MinZoom;
}
public class ControlPointList : CompositeDrawable public class ControlPointList : CompositeDrawable
{ {
private OsuButton deleteButton; private OsuButton deleteButton;
@ -129,11 +142,12 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true);
controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
controlGroups.CollectionChanged += (sender, args) => createContent();
createContent();
}
private void createContent() => table.ControlGroups = controlGroups; controlGroups.BindCollectionChanged((sender, args) =>
{
table.ControlGroups = controlGroups;
}, true);
}
private void delete() private void delete()
{ {

View File

@ -103,12 +103,17 @@ namespace osu.Game.Screens.Edit.Timing
private const double sane_maximum = 240; private const double sane_maximum = 240;
private readonly BindableNumber<double> beatLengthBindable = new TimingControlPoint().BeatLengthBindable; private readonly BindableNumber<double> beatLengthBindable = new TimingControlPoint().BeatLengthBindable;
private readonly BindableDouble bpmBindable = new BindableDouble();
private readonly BindableDouble bpmBindable = new BindableDouble(60000 / TimingControlPoint.DEFAULT_BEAT_LENGTH)
{
MinValue = sane_minimum,
MaxValue = sane_maximum,
};
public BPMSlider() public BPMSlider()
{ {
beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true); beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true);
bpmBindable.BindValueChanged(bpm => bpmBindable.Default = beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
base.Bindable = bpmBindable; base.Bindable = bpmBindable;
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -14,12 +16,17 @@ namespace osu.Game.Screens.Play
/// <see cref="IFrameBasedClock"/>, as this should only be done once to ensure accuracy. /// <see cref="IFrameBasedClock"/>, as this should only be done once to ensure accuracy.
/// </remarks> /// </remarks>
/// </summary> /// </summary>
public class GameplayClock : IFrameBasedClock public class GameplayClock : IFrameBasedClock, ISamplePlaybackDisabler
{ {
private readonly IFrameBasedClock underlyingClock; private readonly IFrameBasedClock underlyingClock;
public readonly BindableBool IsPaused = new BindableBool(); public readonly BindableBool IsPaused = new BindableBool();
/// <summary>
/// All adjustments applied to this clock which don't come from gameplay or mods.
/// </summary>
public virtual IEnumerable<Bindable<double>> NonGameplayAdjustments => Enumerable.Empty<Bindable<double>>();
public GameplayClock(IFrameBasedClock underlyingClock) public GameplayClock(IFrameBasedClock underlyingClock)
{ {
this.underlyingClock = underlyingClock; this.underlyingClock = underlyingClock;
@ -29,6 +36,23 @@ namespace osu.Game.Screens.Play
public double Rate => underlyingClock.Rate; public double Rate => underlyingClock.Rate;
/// <summary>
/// The rate of gameplay when playback is at 100%.
/// This excludes any seeking / user adjustments.
/// </summary>
public double TrueGameplayRate
{
get
{
double baseRate = Rate;
foreach (var adjustment in NonGameplayAdjustments)
baseRate /= adjustment.Value;
return baseRate;
}
}
public bool IsRunning => underlyingClock.IsRunning; public bool IsRunning => underlyingClock.IsRunning;
/// <summary> /// <summary>
@ -48,5 +72,7 @@ namespace osu.Game.Screens.Play
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public IClock Source => underlyingClock; public IClock Source => underlyingClock;
public IBindable<bool> SamplePlaybackDisabled => IsPaused;
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -50,8 +51,11 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// The final clock which is exposed to underlying components. /// The final clock which is exposed to underlying components.
/// </summary> /// </summary>
[Cached] public GameplayClock GameplayClock => localGameplayClock;
public readonly GameplayClock GameplayClock;
[Cached(typeof(GameplayClock))]
[Cached(typeof(ISamplePlaybackDisabler))]
private readonly LocalGameplayClock localGameplayClock;
private Bindable<double> userAudioOffset; private Bindable<double> userAudioOffset;
@ -79,7 +83,7 @@ namespace osu.Game.Screens.Play
userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock);
// the clock to be exposed via DI to children. // the clock to be exposed via DI to children.
GameplayClock = new GameplayClock(userOffsetClock); localGameplayClock = new LocalGameplayClock(userOffsetClock);
GameplayClock.IsPaused.BindTo(IsPaused); GameplayClock.IsPaused.BindTo(IsPaused);
} }
@ -200,7 +204,9 @@ namespace osu.Game.Screens.Play
protected override void Update() protected override void Update()
{ {
if (!IsPaused.Value) if (!IsPaused.Value)
{
userOffsetClock.ProcessFrame(); userOffsetClock.ProcessFrame();
}
base.Update(); base.Update();
} }
@ -215,6 +221,9 @@ namespace osu.Game.Screens.Play
track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust);
localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate);
speedAdjustmentsApplied = true; speedAdjustmentsApplied = true;
} }
@ -231,9 +240,24 @@ namespace osu.Game.Screens.Play
track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust);
localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate);
speedAdjustmentsApplied = false; speedAdjustmentsApplied = false;
} }
private class LocalGameplayClock : GameplayClock
{
public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments;
public LocalGameplayClock(FramedOffsetClock underlyingClock)
: base(underlyingClock)
{
}
}
private class HardwareCorrectionOffsetClock : FramedOffsetClock private class HardwareCorrectionOffsetClock : FramedOffsetClock
{ {
// we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this.

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play
{
/// <summary>
/// Allows a component to disable sample playback dynamically as required.
/// Handled by <see cref="PausableSkinnableSound"/>.
/// </summary>
public interface ISamplePlaybackDisabler
{
/// <summary>
/// Whether sample playback should be disabled (or paused for looping samples).
/// </summary>
IBindable<bool> SamplePlaybackDisabled { get; }
}
}

View File

@ -191,9 +191,25 @@ namespace osu.Game.Screens.Play
dependencies.CacheAs(gameplayBeatmap); dependencies.CacheAs(gameplayBeatmap);
addUnderlayComponents(GameplayClockContainer); var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap);
addOverlayComponents(GameplayClockContainer, Beatmap.Value); // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources.
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
// load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
GameplayClockContainer.Add(beatmapSkinProvider.WithChild(rulesetSkinProvider));
rulesetSkinProvider.AddRange(new[]
{
// underlay and gameplay should have access the to skinning sources.
createUnderlayComponents(),
createGameplayComponents(Beatmap.Value, playableBeatmap)
});
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
GameplayClockContainer.Add(createOverlayComponents(Beatmap.Value));
if (!DrawableRuleset.AllowGameplayOverlays) if (!DrawableRuleset.AllowGameplayOverlays)
{ {
@ -238,45 +254,31 @@ namespace osu.Game.Screens.Play
breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
} }
private void addUnderlayComponents(Container target) private Drawable createUnderlayComponents() =>
DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both };
private Drawable createGameplayComponents(WorkingBeatmap working, IBeatmap playableBeatmap) => new ScalingContainer(ScalingMode.Gameplay)
{ {
target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }); Children = new Drawable[]
}
private void addGameplayComponents(Container target, WorkingBeatmap working, IBeatmap playableBeatmap)
{
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(working.Skin);
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources.
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
// load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
target.Add(new ScalingContainer(ScalingMode.Gameplay)
.WithChild(beatmapSkinProvider
.WithChild(target = rulesetSkinProvider)));
target.AddRange(new Drawable[]
{ {
DrawableRuleset, DrawableRuleset.With(r =>
r.FrameStableComponents.Children = new Drawable[]
{
ScoreProcessor,
HealthProcessor,
breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
{
Breaks = working.Beatmap.Breaks
}
}),
new ComboEffects(ScoreProcessor) new ComboEffects(ScoreProcessor)
}); }
};
DrawableRuleset.FrameStableComponents.AddRange(new Drawable[] private Drawable createOverlayComponents(WorkingBeatmap working) => new Container
{
ScoreProcessor,
HealthProcessor,
breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
{
Breaks = working.Beatmap.Breaks
}
});
}
private void addOverlayComponents(Container target, WorkingBeatmap working)
{ {
target.AddRange(new[] RelativeSizeAxes = Axes.Both,
Children = new[]
{ {
DimmableStoryboard.OverlayLayerContainer.CreateProxy(), DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
@ -342,8 +344,8 @@ namespace osu.Game.Screens.Play
}, },
}, },
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
}); }
} };
private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime) private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime)
{ {

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Screens.Play;
namespace osu.Game.Skinning
{
public class PausableSkinnableSound : SkinnableSound
{
protected bool RequestedPlaying { get; private set; }
public PausableSkinnableSound(ISampleInfo hitSamples)
: base(hitSamples)
{
}
public PausableSkinnableSound(IEnumerable<ISampleInfo> hitSamples)
: base(hitSamples)
{
}
private readonly IBindable<bool> samplePlaybackDisabled = new Bindable<bool>();
[BackgroundDependencyLoader(true)]
private void load(ISamplePlaybackDisabler samplePlaybackDisabler)
{
// if in a gameplay context, pause sample playback when gameplay is paused.
if (samplePlaybackDisabler != null)
{
samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
samplePlaybackDisabled.BindValueChanged(disabled =>
{
if (RequestedPlaying)
{
if (disabled.NewValue)
base.Stop();
// it's not easy to know if a sample has finished playing (to end).
// to keep things simple only resume playing looping samples.
else if (Looping)
base.Play();
}
});
}
}
public override void Play()
{
RequestedPlaying = true;
if (samplePlaybackDisabled.Value)
return;
base.Play();
}
public override void Stop()
{
RequestedPlaying = false;
base.Stop();
}
}
}

View File

@ -11,7 +11,6 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Screens.Play;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
@ -22,8 +21,6 @@ namespace osu.Game.Skinning
[Resolved] [Resolved]
private ISampleStore samples { get; set; } private ISampleStore samples { get; set; }
private bool requestedPlaying;
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
@ -37,7 +34,7 @@ namespace osu.Game.Skinning
/// </remarks> /// </remarks>
protected bool PlayWhenZeroVolume => Looping; protected bool PlayWhenZeroVolume => Looping;
private readonly AudioContainer<DrawableSample> samplesContainer; protected readonly AudioContainer<DrawableSample> SamplesContainer;
public SkinnableSound(ISampleInfo hitSamples) public SkinnableSound(ISampleInfo hitSamples)
: this(new[] { hitSamples }) : this(new[] { hitSamples })
@ -47,28 +44,7 @@ namespace osu.Game.Skinning
public SkinnableSound(IEnumerable<ISampleInfo> hitSamples) public SkinnableSound(IEnumerable<ISampleInfo> hitSamples)
{ {
this.hitSamples = hitSamples.ToArray(); this.hitSamples = hitSamples.ToArray();
InternalChild = samplesContainer = new AudioContainer<DrawableSample>(); InternalChild = SamplesContainer = new AudioContainer<DrawableSample>();
}
private Bindable<bool> gameplayClockPaused;
[BackgroundDependencyLoader(true)]
private void load(GameplayClock gameplayClock)
{
// if in a gameplay context, pause sample playback when gameplay is paused.
gameplayClockPaused = gameplayClock?.IsPaused.GetBoundCopy();
gameplayClockPaused?.BindValueChanged(paused =>
{
if (requestedPlaying)
{
if (paused.NewValue)
stop();
// it's not easy to know if a sample has finished playing (to end).
// to keep things simple only resume playing looping samples.
else if (Looping)
play();
}
});
} }
private bool looping; private bool looping;
@ -82,34 +58,22 @@ namespace osu.Game.Skinning
looping = value; looping = value;
samplesContainer.ForEach(c => c.Looping = looping); SamplesContainer.ForEach(c => c.Looping = looping);
} }
} }
public void Play() public virtual void Play()
{ {
requestedPlaying = true; SamplesContainer.ForEach(c =>
play();
}
private void play()
{
samplesContainer.ForEach(c =>
{ {
if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0)
c.Play(); c.Play();
}); });
} }
public void Stop() public virtual void Stop()
{ {
requestedPlaying = false; SamplesContainer.ForEach(c => c.Stop());
stop();
}
private void stop()
{
samplesContainer.ForEach(c => c.Stop());
} }
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin, bool allowFallback)
@ -138,33 +102,33 @@ namespace osu.Game.Skinning
return ch; return ch;
}).Where(c => c != null); }).Where(c => c != null);
samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); SamplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c));
// Start playback internally for the new samples if the previous ones were playing beforehand. // Start playback internally for the new samples if the previous ones were playing beforehand.
if (wasPlaying) if (wasPlaying)
play(); Play();
} }
#region Re-expose AudioContainer #region Re-expose AudioContainer
public BindableNumber<double> Volume => samplesContainer.Volume; public BindableNumber<double> Volume => SamplesContainer.Volume;
public BindableNumber<double> Balance => samplesContainer.Balance; public BindableNumber<double> Balance => SamplesContainer.Balance;
public BindableNumber<double> Frequency => samplesContainer.Frequency; public BindableNumber<double> Frequency => SamplesContainer.Frequency;
public BindableNumber<double> Tempo => samplesContainer.Tempo; public BindableNumber<double> Tempo => SamplesContainer.Tempo;
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
=> samplesContainer.AddAdjustment(type, adjustBindable); => SamplesContainer.AddAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
=> samplesContainer.RemoveAdjustment(type, adjustBindable); => SamplesContainer.RemoveAdjustment(type, adjustBindable);
public void RemoveAllAdjustments(AdjustableProperty type) public void RemoveAllAdjustments(AdjustableProperty type)
=> samplesContainer.RemoveAllAdjustments(type); => SamplesContainer.RemoveAllAdjustments(type);
public bool IsPlaying => samplesContainer.Any(s => s.Playing); public bool IsPlaying => SamplesContainer.Any(s => s.Playing);
#endregion #endregion
} }

View File

@ -4,15 +4,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Skinning;
namespace osu.Game.Storyboards.Drawables namespace osu.Game.Storyboards.Drawables
{ {
public class DrawableStoryboardSample : Component public class DrawableStoryboardSample : PausableSkinnableSound
{ {
/// <summary> /// <summary>
/// The amount of time allowable beyond the start time of the sample, for the sample to start. /// The amount of time allowable beyond the start time of the sample, for the sample to start.
@ -21,38 +19,37 @@ namespace osu.Game.Storyboards.Drawables
private readonly StoryboardSampleInfo sampleInfo; private readonly StoryboardSampleInfo sampleInfo;
protected SampleChannel Channel { get; private set; }
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo) public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo)
: base(sampleInfo)
{ {
this.sampleInfo = sampleInfo; this.sampleInfo = sampleInfo;
LifetimeStart = sampleInfo.StartTime; LifetimeStart = sampleInfo.StartTime;
} }
[BackgroundDependencyLoader] [Resolved]
private void load(IBindable<WorkingBeatmap> beatmap, IBindable<IReadOnlyList<Mod>> mods) private IBindable<IReadOnlyList<Mod>> mods { get; set; }
{
Channel = beatmap.Value.Skin.GetSample(sampleInfo);
if (Channel == null)
return;
Channel.Volume.Value = sampleInfo.Volume / 100.0; protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
foreach (var mod in mods.Value.OfType<IApplicableToSample>()) foreach (var mod in mods.Value.OfType<IApplicableToSample>())
mod.ApplyToSample(Channel); {
foreach (var sample in SamplesContainer)
mod.ApplyToSample(sample);
}
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
// TODO: this logic will need to be consolidated with other game samples like hit sounds.
if (Time.Current < sampleInfo.StartTime) if (Time.Current < sampleInfo.StartTime)
{ {
// We've rewound before the start time of the sample // We've rewound before the start time of the sample
Channel?.Stop(); Stop();
// In the case that the user fast-forwards to a point far beyond the start time of the sample, // In the case that the user fast-forwards to a point far beyond the start time of the sample,
// we want to be able to fall into the if-conditional below (therefore we must not have a life time end) // we want to be able to fall into the if-conditional below (therefore we must not have a life time end)
@ -63,8 +60,8 @@ namespace osu.Game.Storyboards.Drawables
{ {
// We've passed the start time of the sample. We only play the sample if we're within an allowable range // We've passed the start time of the sample. We only play the sample if we're within an allowable range
// from the sample's start, to reduce layering if we've been fast-forwarded far into the future // from the sample's start, to reduce layering if we've been fast-forwarded far into the future
if (Time.Current - sampleInfo.StartTime < allowable_late_start) if (!RequestedPlaying && Time.Current - sampleInfo.StartTime < allowable_late_start)
Channel?.Play(); Play();
// In the case that the user rewinds to a point far behind the start time of the sample, // In the case that the user rewinds to a point far behind the start time of the sample,
// we want to be able to fall into the if-conditional above (therefore we must not have a life time start) // we want to be able to fall into the if-conditional above (therefore we must not have a life time start)
@ -72,13 +69,5 @@ namespace osu.Game.Storyboards.Drawables
LifetimeEnd = sampleInfo.StartTime; LifetimeEnd = sampleInfo.StartTime;
} }
} }
protected override void Dispose(bool isDisposing)
{
Channel?.Stop();
Channel = null;
base.Dispose(isDisposing);
}
} }
} }

View File

@ -20,6 +20,8 @@ namespace osu.Game.Tests.Visual
protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor(); protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor();
protected new readonly EditorClock Clock; protected new readonly EditorClock Clock;
protected virtual bool ScrollUsingMouseWheel => true;
protected EditorClockTestScene() protected EditorClockTestScene()
{ {
Clock = new EditorClock(new ControlPointInfo(), 5000, BeatDivisor) { IsCoupled = false }; Clock = new EditorClock(new ControlPointInfo(), 5000, BeatDivisor) { IsCoupled = false };
@ -57,6 +59,9 @@ namespace osu.Game.Tests.Visual
protected override bool OnScroll(ScrollEvent e) protected override bool OnScroll(ScrollEvent e)
{ {
if (!ScrollUsingMouseWheel)
return false;
if (e.ScrollDelta.Y > 0) if (e.ScrollDelta.Y > 0)
Clock.SeekBackward(true); Clock.SeekBackward(true);
else else

View File

@ -26,6 +26,8 @@ namespace osu.Game.Users
private void load() private void load()
{ {
Background.Width = 0.5f; Background.Width = 0.5f;
Background.Origin = Anchor.CentreRight;
Background.Anchor = Anchor.CentreRight;
Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.3f)); Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.3f));
} }

View File

@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.925.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.930.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="Sentry" Version="2.1.6" /> <PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.925.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.930.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
</ItemGroup> </ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
@ -80,7 +80,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.925.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.930.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />