diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 171ba91ada..02dff6993d 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -333,6 +333,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual("hit_2.wav", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); Assert.AreEqual("hit_1.wav", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); + Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume); } SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); diff --git a/osu.Game.Tests/Resources/hitobject-file-samples.osu b/osu.Game.Tests/Resources/hitobject-file-samples.osu index 588672e2d9..7c69f259b8 100644 --- a/osu.Game.Tests/Resources/hitobject-file-samples.osu +++ b/osu.Game.Tests/Resources/hitobject-file-samples.osu @@ -13,4 +13,4 @@ SampleSet: Normal 255,193,2170,1,0,0:0:0:0:hit_1.wav 256,191,2638,5,0,0:0:0:0:hit_2.wav 255,193,3107,1,0,0:0:0:0: -256,191,3576,1,0,0:0:0:0:hit_1.wav +256,191,3576,1,0,0:0:0:70:hit_1.wav diff --git a/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs b/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs index d850c58f09..5484824c5b 100644 --- a/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs +++ b/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs @@ -188,10 +188,10 @@ namespace osu.Game.Tests.Visual public void PauseTest() { performFullSetup(true); - AddStep("Pause", () => player.CurrentPauseContainer.Pause()); + AddStep("Pause", () => player.CurrentPausableGameplayContainer.Pause()); waitForDim(); AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed()); - AddStep("Unpause", () => player.CurrentPauseContainer.Resume()); + AddStep("Unpause", () => player.CurrentPausableGameplayContainer.Resume()); waitForDim(); AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed()); } @@ -328,7 +328,7 @@ namespace osu.Game.Tests.Visual }; } - public PauseContainer CurrentPauseContainer => PauseContainer; + public PausableGameplayContainer CurrentPausableGameplayContainer => PausableGameplayContainer; public UserDimContainer CurrentStoryboardContainer => StoryboardContainer; diff --git a/osu.Game.Tests/Visual/TestCaseDirectPanel.cs b/osu.Game.Tests/Visual/TestCaseDirectPanel.cs new file mode 100644 index 0000000000..beb88ac56c --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseDirectPanel.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays.Direct; +using osu.Game.Rulesets.Osu; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseDirectPanel : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DirectGridPanel), + typeof(DirectListPanel), + typeof(IconPill) + }; + + [BackgroundDependencyLoader] + private void load() + { + var beatmap = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, null); + beatmap.BeatmapSetInfo.OnlineInfo.HasVideo = true; + beatmap.BeatmapSetInfo.OnlineInfo.HasStoryboard = true; + + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(20), + Spacing = new Vector2(0, 20), + Children = new Drawable[] + { + new DirectGridPanel(beatmap.BeatmapSetInfo), + new DirectListPanel(beatmap.BeatmapSetInfo) + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs index a21573236a..93a059d214 100644 --- a/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs @@ -17,15 +17,15 @@ namespace osu.Game.Tests.Visual [Description("player pause/fail screens")] public class TestCaseGameplayMenuOverlay : ManualInputManagerTestCase { - public override IReadOnlyList RequiredTypes => new[] { typeof(FailOverlay), typeof(PauseContainer) }; + public override IReadOnlyList RequiredTypes => new[] { typeof(FailOverlay), typeof(PausableGameplayContainer) }; private FailOverlay failOverlay; - private PauseContainer.PauseOverlay pauseOverlay; + private PausableGameplayContainer.PauseOverlay pauseOverlay; [BackgroundDependencyLoader] private void load() { - Add(pauseOverlay = new PauseContainer.PauseOverlay + Add(pauseOverlay = new PausableGameplayContainer.PauseOverlay { OnResume = () => Logger.Log(@"Resume"), OnRetry = () => Logger.Log(@"Retry"), diff --git a/osu.Game.Tests/Visual/TestCaseSongProgress.cs b/osu.Game.Tests/Visual/TestCaseSongProgress.cs index 9845df7461..cdb1cd2286 100644 --- a/osu.Game.Tests/Visual/TestCaseSongProgress.cs +++ b/osu.Game.Tests/Visual/TestCaseSongProgress.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.MathUtils; using osu.Framework.Timing; @@ -19,14 +20,20 @@ namespace osu.Game.Tests.Visual private readonly StopwatchClock clock; + [Cached] + private readonly GameplayClock gameplayClock; + + private readonly FramedClock framedClock; + public TestCaseSongProgress() { clock = new StopwatchClock(true); + gameplayClock = new GameplayClock(framedClock = new FramedClock(clock)); + Add(progress = new SongProgress { RelativeSizeAxes = Axes.X, - AudioClock = new StopwatchClock(true), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, }); @@ -68,8 +75,13 @@ namespace osu.Game.Tests.Visual progress.Objects = objects; graph.Objects = objects; - progress.AudioClock = clock; - progress.OnSeek = pos => clock.Seek(pos); + progress.RequestSeek = pos => clock.Seek(pos); + } + + protected override void Update() + { + base.Update(); + framedClock.ProcessFrame(); } private class TestSongProgressGraph : SongProgressGraph diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 38df0efd6f..f5b6e185c7 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -213,12 +213,6 @@ namespace osu.Game.Online.Leaderboards pendingUpdateScores?.Cancel(); pendingUpdateScores = Schedule(() => { - if (api?.IsLoggedIn != true) - { - PlaceholderState = PlaceholderState.NotLoggedIn; - return; - } - PlaceholderState = PlaceholderState.Retrieving; loading.Show(); @@ -231,6 +225,12 @@ namespace osu.Game.Online.Leaderboards if (getScoresRequest == null) return; + if (api?.IsLoggedIn != true) + { + PlaceholderState = PlaceholderState.NotLoggedIn; + return; + } + getScoresRequest.Failure += e => Schedule(() => { if (e is OperationCanceledException) @@ -243,6 +243,11 @@ namespace osu.Game.Online.Leaderboards }); } + /// + /// Performs a fetch/refresh of scores to be displayed. + /// + /// A callback which should be called when fetching is completed. Scheduling is not required. + /// An responsible for the fetch operation. This will be queued and performed automatically. protected abstract APIRequest FetchScores(Action> scoresCallback); private Placeholder currentPlaceholder; diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs index 7bf372dff7..01393ad98b 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -13,6 +13,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; namespace osu.Game.Overlays.Direct { @@ -23,6 +24,7 @@ namespace osu.Game.Overlays.Direct private const float vertical_padding = 5; private const float height = 70; + private FillFlowContainer statusContainer; private PlayButton playButton; private Box progressBar; @@ -108,10 +110,24 @@ namespace osu.Game.Overlays.Direct }, new FillFlowContainer { - AutoSizeAxes = Axes.X, - Height = 20, - Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding }, - Children = GetDifficultyIcons(), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + statusContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Vertical = vertical_padding, Horizontal = 5 }, + Spacing = new Vector2(5), + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.X, + Height = 20, + Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding }, + Children = GetDifficultyIcons(), + }, + }, }, }, }, @@ -194,6 +210,23 @@ namespace osu.Game.Overlays.Direct Colour = colours.Yellow, }, }); + + if (SetInfo.OnlineInfo?.HasVideo ?? false) + { + statusContainer.Add(new IconPill(FontAwesome.fa_film) { IconSize = new Vector2(20) }); + } + + if (SetInfo.OnlineInfo?.HasStoryboard ?? false) + { + statusContainer.Add(new IconPill(FontAwesome.fa_image) { IconSize = new Vector2(20) }); + } + + statusContainer.Add(new BeatmapSetOnlineStatusPill + { + TextSize = 12, + TextPadding = new MarginPadding { Horizontal = 10, Vertical = 4 }, + Status = SetInfo.OnlineInfo?.Status ?? BeatmapSetOnlineStatus.None, + }); } } } diff --git a/osu.Game/Overlays/Direct/IconPill.cs b/osu.Game/Overlays/Direct/IconPill.cs index 83ee0cb6c5..e7f516f449 100644 --- a/osu.Game/Overlays/Direct/IconPill.cs +++ b/osu.Game/Overlays/Direct/IconPill.cs @@ -12,6 +12,14 @@ namespace osu.Game.Overlays.Direct { public class IconPill : CircularContainer { + public Vector2 IconSize + { + get => iconContainer.Size; + set => iconContainer.Size = value; + } + + private readonly Container iconContainer; + public IconPill(FontAwesome icon) { AutoSizeAxes = Axes.Both; @@ -25,16 +33,16 @@ namespace osu.Game.Overlays.Direct Colour = Color4.Black, Alpha = 0.5f, }, - new Container + iconContainer = new Container { - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding(5), + Size = new Vector2(22), + Padding = new MarginPadding(5), Child = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, Icon = icon, - Size = new Vector2(12), }, }, }; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index b9177a325c..e397139843 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -301,7 +301,16 @@ namespace osu.Game.Rulesets.Objects.Legacy { // Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario if (!string.IsNullOrEmpty(bankInfo.Filename)) - return new List { new FileSampleInfo { Filename = bankInfo.Filename } }; + { + return new List + { + new FileSampleInfo + { + Filename = bankInfo.Filename, + Volume = bankInfo.Volume + } + }; + } var soundTypes = new List { diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index fc61d41ab4..87220a37e8 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -41,6 +41,7 @@ namespace osu.Game.Rulesets.UI protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) { InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique); + gameplayClock = new GameplayClock(framedClock = new FramedClock(manualClock = new ManualClock())); } #region Action mapping (for replays) @@ -86,22 +87,28 @@ namespace osu.Game.Rulesets.UI #region Clock control - private ManualClock clock; - private IFrameBasedClock parentClock; + private readonly ManualClock manualClock; + + private readonly FramedClock framedClock; + + [Cached] + private GameplayClock gameplayClock; + + private IFrameBasedClock parentGameplayClock; + + [BackgroundDependencyLoader(true)] + private void load(OsuConfigManager config, GameplayClock clock) + { + mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons); + + if (clock != null) + parentGameplayClock = clock; + } protected override void LoadComplete() { base.LoadComplete(); - - //our clock will now be our parent's clock, but we want to replace this to allow manual control. - parentClock = Clock; - - ProcessCustomClock = false; - Clock = new FramedClock(clock = new ManualClock - { - CurrentTime = parentClock.CurrentTime, - Rate = parentClock.Rate, - }); + setClock(); } /// @@ -147,25 +154,28 @@ namespace osu.Game.Rulesets.UI private void updateClock() { - if (parentClock == null) return; + if (parentGameplayClock == null) + setClock(); // LoadComplete may not be run yet, but we still want the clock. - clock.Rate = parentClock.Rate; - clock.IsRunning = parentClock.IsRunning; + validState = true; - var newProposedTime = parentClock.CurrentTime; + manualClock.Rate = parentGameplayClock.Rate; + manualClock.IsRunning = parentGameplayClock.IsRunning; + + var newProposedTime = parentGameplayClock.CurrentTime; try { - if (Math.Abs(clock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) + if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) { - newProposedTime = clock.Rate > 0 - ? Math.Min(newProposedTime, clock.CurrentTime + sixty_frame_time) - : Math.Max(newProposedTime, clock.CurrentTime - sixty_frame_time); + newProposedTime = manualClock.Rate > 0 + ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time) + : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time); } if (!isAttached) { - clock.CurrentTime = newProposedTime; + manualClock.CurrentTime = newProposedTime; } else { @@ -177,35 +187,39 @@ namespace osu.Game.Rulesets.UI validState = false; requireMoreUpdateLoops = true; - clock.CurrentTime = newProposedTime; + manualClock.CurrentTime = newProposedTime; return; } - clock.CurrentTime = newTime.Value; + manualClock.CurrentTime = newTime.Value; } - requireMoreUpdateLoops = clock.CurrentTime != parentClock.CurrentTime; + requireMoreUpdateLoops = manualClock.CurrentTime != parentGameplayClock.CurrentTime; } finally { // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed - Clock.ProcessFrame(); + framedClock.ProcessFrame(); } } + private void setClock() + { + // in case a parent gameplay clock isn't available, just use the parent clock. + if (parentGameplayClock == null) + parentGameplayClock = Clock; + + Clock = gameplayClock; + ProcessCustomClock = false; + } + #endregion #region Setting application (disables etc.) private Bindable mouseDisabled; - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons); - } - protected override bool Handle(UIEvent e) { switch (e) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 024ce01dc6..d390787090 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -40,13 +41,7 @@ namespace osu.Game.Screens.Play private readonly BreakInfo info; private readonly BreakArrows breakArrows; - public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) - : this(letterboxing) - { - bindProcessor(scoreProcessor); - } - - public BreakOverlay(bool letterboxing) + public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor = null) { RelativeSizeAxes = Axes.Both; Child = fadeContainer = new Container @@ -98,6 +93,14 @@ namespace osu.Game.Screens.Play } } }; + + if (scoreProcessor != null) bindProcessor(scoreProcessor); + } + + [BackgroundDependencyLoader(true)] + private void load(GameplayClock clock) + { + if (clock != null) Clock = clock; } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs new file mode 100644 index 0000000000..0400bfbc27 --- /dev/null +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -0,0 +1,43 @@ +// 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.Timing; + +namespace osu.Game.Screens.Play +{ + /// + /// A clock which is used for gameplay elements that need to follow audio time 1:1. + /// Exposed via DI by . + /// + /// The main purpose of this clock is to stop components using it from accidentally processing the main + /// , as this should only be done once to ensure accuracy. + /// + /// + public class GameplayClock : IFrameBasedClock + { + private readonly IFrameBasedClock underlyingClock; + + public GameplayClock(IFrameBasedClock underlyingClock) + { + this.underlyingClock = underlyingClock; + } + + public double CurrentTime => underlyingClock.CurrentTime; + + public double Rate => underlyingClock.Rate; + + public bool IsRunning => underlyingClock.IsRunning; + + public void ProcessFrame() + { + // we do not want to process the underlying clock. + // this is handled by PauseContainer. + } + + public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; + + public double FramesPerSecond => underlyingClock.FramesPerSecond; + + public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 130d2ecc95..a19b0d1e5c 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play private static bool hasShownNotificationOnce; - public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IClock offsetClock, IAdjustableClock adjustableClock) + public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IAdjustableClock adjustableClock) { RelativeSizeAxes = Axes.Both; @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Play Direction = FillDirection.Vertical, Children = new Drawable[] { - KeyCounter = CreateKeyCounter(adjustableClock as IFrameBasedClock), + KeyCounter = CreateKeyCounter(), HoldToQuit = CreateHoldForMenuButton(), } } @@ -91,9 +91,8 @@ namespace osu.Game.Screens.Play BindRulesetContainer(rulesetContainer); Progress.Objects = rulesetContainer.Objects; - Progress.AudioClock = offsetClock; Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value; - Progress.OnSeek = pos => adjustableClock.Seek(pos); + Progress.RequestSeek = pos => adjustableClock.Seek(pos); ModDisplay.Current.BindTo(working.Mods); @@ -202,13 +201,12 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding { Top = 20 } }; - protected virtual KeyCounterCollection CreateKeyCounter(IFrameBasedClock offsetClock) => new KeyCounterCollection + protected virtual KeyCounterCollection CreateKeyCounter() => new KeyCounterCollection { FadeTime = 50, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Margin = new MarginPadding(10), - AudioClock = offsetClock }; protected virtual SongProgress CreateProgress() => new SongProgress diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/KeyCounter.cs index 0e1f938137..0626c40334 100644 --- a/osu.Game/Screens/Play/KeyCounter.cs +++ b/osu.Game/Screens/Play/KeyCounter.cs @@ -71,9 +71,12 @@ namespace osu.Game.Screens.Play Name = name; } - [BackgroundDependencyLoader] - private void load(TextureStore textures) + [BackgroundDependencyLoader(true)] + private void load(TextureStore textures, GameplayClock clock) { + if (clock != null) + Clock = clock; + Children = new Drawable[] { buttonSprite = new Sprite diff --git a/osu.Game/Screens/Play/KeyCounterCollection.cs b/osu.Game/Screens/Play/KeyCounterCollection.cs index 0259258636..1b43737731 100644 --- a/osu.Game/Screens/Play/KeyCounterCollection.cs +++ b/osu.Game/Screens/Play/KeyCounterCollection.cs @@ -8,7 +8,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Framework.Timing; using osu.Game.Configuration; using osuTK; using osuTK.Graphics; @@ -37,9 +36,6 @@ namespace osu.Game.Screens.Play key.FadeTime = FadeTime; key.KeyDownTextColor = KeyDownTextColor; key.KeyUpTextColor = KeyUpTextColor; - // Use the same clock object as SongProgress for saving KeyCounter state - if (AudioClock != null) - key.Clock = AudioClock; } public void ResetCount() @@ -125,8 +121,6 @@ namespace osu.Game.Screens.Play public override bool HandleNonPositionalInput => receptor == null; public override bool HandlePositionalInput => receptor == null; - public IFrameBasedClock AudioClock { get; set; } - private Receptor receptor; public Receptor GetReceptor() diff --git a/osu.Game/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PausableGameplayContainer.cs similarity index 74% rename from osu.Game/Screens/Play/PauseContainer.cs rename to osu.Game/Screens/Play/PausableGameplayContainer.cs index 8961d91763..fa475deb34 100644 --- a/osu.Game/Screens/Play/PauseContainer.cs +++ b/osu.Game/Screens/Play/PausableGameplayContainer.cs @@ -14,10 +14,11 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play { /// - /// A container which handles pausing children, displaying a pause overlay with choices etc. + /// A container which handles pausing children, displaying a pause overlay with choices and processing the clock. + /// Exposes a to children via DI. /// This alleviates a lot of the intricate pause logic from being in /// - public class PauseContainer : Container + public class PausableGameplayContainer : Container { public readonly BindableBool IsPaused = new BindableBool(); @@ -43,24 +44,32 @@ namespace osu.Game.Screens.Play public Action OnRetry; public Action OnQuit; - private readonly FramedClock framedClock; - private readonly DecoupleableInterpolatingFramedClock decoupledClock; + private readonly FramedClock offsetClock; + private readonly DecoupleableInterpolatingFramedClock adjustableClock; /// - /// Creates a new . + /// The final clock which is exposed to underlying components. /// - /// The gameplay clock. This is the clock that will process frames. - /// The seekable clock. This is the clock that will be paused and resumed. - public PauseContainer(FramedClock framedClock, DecoupleableInterpolatingFramedClock decoupledClock) + [Cached] + private readonly GameplayClock gameplayClock; + + /// + /// Creates a new . + /// + /// The gameplay clock. This is the clock that will process frames. Includes user/system offsets. + /// The seekable clock. This is the clock that will be paused and resumed. Should not be processed (it is processed automatically by ). + public PausableGameplayContainer(FramedClock offsetClock, DecoupleableInterpolatingFramedClock adjustableClock) { - this.framedClock = framedClock; - this.decoupledClock = decoupledClock; + this.offsetClock = offsetClock; + this.adjustableClock = adjustableClock; + + gameplayClock = new GameplayClock(offsetClock); RelativeSizeAxes = Axes.Both; AddInternal(content = new Container { - Clock = this.framedClock, + Clock = this.offsetClock, ProcessCustomClock = false, RelativeSizeAxes = Axes.Both }); @@ -84,7 +93,7 @@ namespace osu.Game.Screens.Play if (IsPaused.Value) return; // stop the seekable clock (stops the audio eventually) - decoupledClock.Stop(); + adjustableClock.Stop(); IsPaused.Value = true; pauseOverlay.Show(); @@ -102,8 +111,8 @@ namespace osu.Game.Screens.Play // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time // This accounts for the audio clock source potentially taking time to enter a completely stopped state - decoupledClock.Seek(decoupledClock.CurrentTime); - decoupledClock.Start(); + adjustableClock.Seek(adjustableClock.CurrentTime); + adjustableClock.Start(); pauseOverlay.Hide(); } @@ -123,7 +132,7 @@ namespace osu.Game.Screens.Play Pause(); if (!IsPaused.Value) - framedClock.ProcessFrame(); + offsetClock.ProcessFrame(); base.Update(); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 1518ee2f32..0f4fa3e762 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Play [Resolved] private ScoreManager scoreManager { get; set; } - protected PauseContainer PauseContainer { get; private set; } + protected PausableGameplayContainer PausableGameplayContainer { get; private set; } private RulesetInfo ruleset; @@ -170,10 +170,10 @@ namespace osu.Game.Screens.Play InternalChildren = new Drawable[] { - PauseContainer = new PauseContainer(offsetClock, adjustableClock) + PausableGameplayContainer = new PausableGameplayContainer(offsetClock, adjustableClock) { Retries = RestartCount, - OnRetry = Restart, + OnRetry = restart, OnQuit = performUserRequestedExit, CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value, Children = new Container[] @@ -191,32 +191,26 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.Centre, Origin = Anchor.Centre, - ProcessCustomClock = false, Breaks = beatmap.Breaks }, new ScalingContainer(ScalingMode.Gameplay) { Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(), }, - HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, offsetClock, adjustableClock) + HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, adjustableClock) { - Clock = Clock, // hud overlay doesn't want to use the audio clock directly - ProcessCustomClock = false, Anchor = Anchor.Centre, Origin = Anchor.Centre }, new SkipOverlay(RulesetContainer.GameplayStartTime) { - Clock = Clock, // skip button doesn't want to use the audio clock directly - ProcessCustomClock = false, - AdjustableClock = adjustableClock, - FramedClock = offsetClock, + RequestSeek = time => adjustableClock.Seek(time) }, } }, failOverlay = new FailOverlay { - OnRetry = Restart, + OnRetry = restart, OnQuit = performUserRequestedExit, }, new HotkeyRetryOverlay @@ -226,7 +220,7 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; fadeOut(true); - Restart(); + restart(); }, } }; @@ -234,7 +228,7 @@ namespace osu.Game.Screens.Play HUDOverlay.HoldToQuit.Action = performUserRequestedExit; HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded); - RulesetContainer.IsPaused.BindTo(PauseContainer.IsPaused); + RulesetContainer.IsPaused.BindTo(PausableGameplayContainer.IsPaused); if (ShowStoryboard.Value) initializeStoryboard(false); @@ -263,7 +257,7 @@ namespace osu.Game.Screens.Play this.Exit(); } - public void Restart() + private void restart() { if (!this.IsCurrentScreen()) return; @@ -367,7 +361,7 @@ namespace osu.Game.Screens.Play this.Delay(750).Schedule(() => { - if (!PauseContainer.IsPaused.Value) + if (!PausableGameplayContainer.IsPaused.Value) { adjustableClock.Start(); } @@ -375,8 +369,8 @@ namespace osu.Game.Screens.Play }); }); - PauseContainer.Alpha = 0; - PauseContainer.FadeIn(750, Easing.OutQuint); + PausableGameplayContainer.Alpha = 0; + PausableGameplayContainer.FadeIn(750, Easing.OutQuint); } public override void OnSuspending(IScreen next) @@ -394,7 +388,7 @@ namespace osu.Game.Screens.Play return true; } - if ((!AllowPause || HasFailed || !ValidForResume || PauseContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PauseContainer?.IsResuming ?? true)) + if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true)) { // In the case of replays, we may have changed the playback rate. applyRateFromMods(); @@ -403,7 +397,7 @@ namespace osu.Game.Screens.Play } if (LoadedBeatmapSuccessfully) - PauseContainer?.Pause(); + PausableGameplayContainer?.Pause(); return true; } @@ -417,7 +411,7 @@ namespace osu.Game.Screens.Play storyboardReplacesBackground.Value = false; } - protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !PauseContainer.IsPaused.Value; + protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !PausableGameplayContainer.IsPaused.Value; private void initializeStoryboard(bool asyncLoad) { diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 5c9acade7b..269baad955 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Menu; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.PlayerSettings; using osuTK; using osuTK.Graphics; @@ -296,6 +297,7 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; private LoadingAnimation loading; private Sprite backgroundSprite; + private ModDisplay modDisplay; public bool Loading { @@ -322,7 +324,7 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load() { - var metadata = beatmap?.BeatmapInfo?.Metadata ?? new BeatmapMetadata(); + var metadata = beatmap.BeatmapInfo?.Metadata ?? new BeatmapMetadata(); AutoSizeAxes = Axes.Both; Children = new Drawable[] @@ -391,6 +393,14 @@ namespace osu.Game.Screens.Play Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, }, + new ModDisplay + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 20 }, + Current = beatmap.Mods + } }, } }; diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 010d9228de..a6e6009b95 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -9,7 +9,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; -using osu.Framework.Timing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Ranking; @@ -27,8 +26,7 @@ namespace osu.Game.Screens.Play { private readonly double startTime; - public IAdjustableClock AdjustableClock; - public IFrameBasedClock FramedClock; + public Action RequestSeek; private Button button; private Box remainingTimeBox; @@ -54,16 +52,13 @@ namespace osu.Game.Screens.Play Origin = Anchor.Centre; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, GameplayClock clock) { var baseClock = Clock; - if (FramedClock != null) - { - Clock = FramedClock; - ProcessCustomClock = false; - } + if (clock != null) + Clock = clock; Children = new Drawable[] { @@ -111,7 +106,7 @@ namespace osu.Game.Screens.Play using (BeginAbsoluteSequence(beginFadeTime)) this.FadeOut(fade_time); - button.Action = () => AdjustableClock?.Seek(startTime - skip_required_cutoff - fade_time); + button.Action = () => RequestSeek?.Invoke(startTime - skip_required_cutoff - fade_time); displayTime = Time.Current; diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index 3c7e3b2067..e3d6ca16a7 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -10,7 +10,6 @@ using osu.Game.Graphics; using osu.Framework.Allocation; using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Timing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; @@ -29,18 +28,11 @@ namespace osu.Game.Screens.Play private readonly SongProgressGraph graph; private readonly SongProgressInfo info; - public Action OnSeek; + public Action RequestSeek; public override bool HandleNonPositionalInput => AllowSeeking; public override bool HandlePositionalInput => AllowSeeking; - private IClock audioClock; - - public IClock AudioClock - { - set => audioClock = info.AudioClock = value; - } - private double lastHitTime => ((objects.Last() as IHasEndTime)?.EndTime ?? objects.Last().StartTime) + 1; private double firstHitTime => objects.First().StartTime; @@ -63,9 +55,14 @@ namespace osu.Game.Screens.Play private readonly BindableBool replayLoaded = new BindableBool(); - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private GameplayClock gameplayClock; + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, GameplayClock clock) { + if (clock != null) + gameplayClock = clock; + graph.FillColour = bar.FillColour = colours.BlueLighter; } @@ -99,7 +96,7 @@ namespace osu.Game.Screens.Play Alpha = 0, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - OnSeek = position => OnSeek?.Invoke(position), + OnSeek = time => RequestSeek?.Invoke(time), }, }; } @@ -157,14 +154,11 @@ namespace osu.Game.Screens.Play if (objects == null) return; - double position = audioClock?.CurrentTime ?? Time.Current; - double progress = (position - firstHitTime) / (lastHitTime - firstHitTime); + double position = gameplayClock?.CurrentTime ?? Time.Current; + double progress = Math.Min(1, (position - firstHitTime) / (lastHitTime - firstHitTime)); - if (progress < 1) - { - bar.CurrentTime = position; - graph.Progress = (int)(graph.ColumnCount * progress); - } + bar.CurrentTime = position; + graph.Progress = (int)(graph.ColumnCount * progress); } } } diff --git a/osu.Game/Screens/Play/SongProgressInfo.cs b/osu.Game/Screens/Play/SongProgressInfo.cs index 369abb53c8..7441c335d2 100644 --- a/osu.Game/Screens/Play/SongProgressInfo.cs +++ b/osu.Game/Screens/Play/SongProgressInfo.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Timing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using System; @@ -27,8 +26,6 @@ namespace osu.Game.Screens.Play private const int margin = 10; - public IClock AudioClock; - public double StartTime { set => startTime = value; @@ -39,9 +36,14 @@ namespace osu.Game.Screens.Play set => endTime = value; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private GameplayClock gameplayClock; + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, GameplayClock clock) { + if (clock != null) + gameplayClock = clock; + Children = new Drawable[] { timeCurrent = new OsuSpriteText @@ -80,7 +82,9 @@ namespace osu.Game.Screens.Play { base.Update(); - double songCurrentTime = AudioClock.CurrentTime - startTime; + var time = gameplayClock?.CurrentTime ?? Time.Current; + + double songCurrentTime = time - startTime; int currentPercent = Math.Max(0, Math.Min(100, (int)(songCurrentTime / songLength * 100))); int currentSecond = (int)Math.Floor(songCurrentTime / 1000.0); @@ -93,7 +97,7 @@ namespace osu.Game.Screens.Play if (currentSecond != previousSecond && songCurrentTime < songLength) { timeCurrent.Text = formatTime(TimeSpan.FromSeconds(currentSecond)); - timeLeft.Text = formatTime(TimeSpan.FromMilliseconds(endTime - AudioClock.CurrentTime)); + timeLeft.Text = formatTime(TimeSpan.FromMilliseconds(endTime - time)); previousSecond = currentSecond; } diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index d1263984ac..b77a8508ad 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.IO; using System.Text; using osu.Game.Beatmaps; @@ -21,6 +22,18 @@ namespace osu.Game.Tests.Beatmaps HitObjects = baseBeatmap.HitObjects; BeatmapInfo.Ruleset = ruleset; + BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; + BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo }; + BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo + { + Status = BeatmapSetOnlineStatus.Ranked, + Covers = new BeatmapSetOnlineCovers + { + Cover = "https://assets.ppy.sh/beatmaps/163112/covers/cover.jpg", + Card = "https://assets.ppy.sh/beatmaps/163112/covers/card.jpg", + List = "https://assets.ppy.sh/beatmaps/163112/covers/list.jpg" + } + }; } private static Beatmap createTestBeatmap()