diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index aa170eae1e..90f1cdb2ea 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -7,7 +7,9 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing.Input;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor;
+using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@@ -21,12 +23,50 @@ namespace osu.Game.Rulesets.Osu.Tests
typeof(CursorTrail)
};
- [BackgroundDependencyLoader]
- private void load()
+ [Cached]
+ private GameplayBeatmap gameplayBeatmap;
+
+ private ClickingCursorContainer lastContainer;
+
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ public TestSceneGameplayCursor()
+ {
+ gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
+ }
+
+ [TestCase(1, 1)]
+ [TestCase(5, 1)]
+ [TestCase(10, 1)]
+ [TestCase(1, 1.5f)]
+ [TestCase(5, 1.5f)]
+ [TestCase(10, 1.5f)]
+ public void TestSizing(int circleSize, float userScale)
+ {
+ AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale));
+ AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
+ AddStep("turn on autosizing", () => config.Set(OsuSetting.AutoCursorSize, true));
+
+ AddStep("load content", loadContent);
+
+ AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
+
+ AddStep("set user scale to 1", () => config.Set(OsuSetting.GameplayCursorSize, 1f));
+ AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize));
+
+ AddStep("turn off autosizing", () => config.Set(OsuSetting.AutoCursorSize, false));
+ AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == 1);
+
+ AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale));
+ AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale);
+ }
+
+ private void loadContent()
{
SetContents(() => new MovingCursorInputManager
{
- Child = new ClickingCursorContainer
+ Child = lastContainer = new ClickingCursorContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 649b01c132..d971e777ec 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -19,33 +19,46 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
+ ///
+ /// How early before a hitobject's start time to trigger a hit.
+ ///
+ private const float relax_leniency = 3;
+
public void Update(Playfield playfield)
{
bool requiresHold = false;
bool requiresHit = false;
- const float relax_leniency = 3;
+ double time = playfield.Clock.CurrentTime;
- foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
+ foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType())
{
- if (!(drawable is DrawableOsuHitObject osuHit))
+ // we are not yet close enough to the object.
+ if (time < h.HitObject.StartTime - relax_leniency)
+ break;
+
+ // already hit or beyond the hittable end time.
+ if (h.IsHit || (h.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime))
continue;
- double time = osuHit.Clock.CurrentTime;
- double relativetime = time - osuHit.HitObject.StartTime;
-
- if (time < osuHit.HitObject.StartTime - relax_leniency) continue;
-
- if ((osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime) || osuHit.IsHit)
- continue;
-
- if (osuHit is DrawableHitCircle && osuHit.IsHovered)
+ switch (h)
{
- Debug.Assert(osuHit.HitObject.HitWindows != null);
- requiresHit |= osuHit.HitObject.HitWindows.CanBeHit(relativetime);
- }
+ case DrawableHitCircle circle:
+ handleHitCircle(circle);
+ break;
- requiresHold |= (osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered)) || osuHit is DrawableSpinner;
+ case DrawableSlider slider:
+ // Handles cases like "2B" beatmaps, where sliders may be overlapping and simply holding is not enough.
+ if (!slider.HeadCircle.IsHit)
+ handleHitCircle(slider.HeadCircle);
+
+ requiresHold |= slider.Ball.IsHovered || h.IsHovered;
+ break;
+
+ case DrawableSpinner _:
+ requiresHold = true;
+ break;
+ }
}
if (requiresHit)
@@ -55,6 +68,15 @@ namespace osu.Game.Rulesets.Osu.Mods
}
addAction(requiresHold);
+
+ void handleHitCircle(DrawableHitCircle circle)
+ {
+ if (!circle.IsHovered)
+ return;
+
+ Debug.Assert(circle.HitObject.HitWindows != null);
+ requiresHit |= circle.HitObject.HitWindows.CanBeHit(time - circle.HitObject.StartTime);
+ }
}
private bool wasHit;
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index 79b5d1b7f8..28600ef55b 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
@@ -29,10 +30,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly Drawable cursorTrail;
- public Bindable CursorScale;
+ public Bindable CursorScale = new BindableFloat(1);
+
private Bindable userCursorScale;
private Bindable autoCursorScale;
- private readonly IBindable beatmap = new Bindable();
public OsuCursorContainer()
{
@@ -43,37 +44,16 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
};
}
+ [Resolved(canBeNull: true)]
+ private GameplayBeatmap beatmap { get; set; }
+
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
[BackgroundDependencyLoader(true)]
- private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig, IBindable beatmap)
+ private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig)
{
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail);
-
- this.beatmap.BindTo(beatmap);
- this.beatmap.ValueChanged += _ => calculateScale();
-
- userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize);
- userCursorScale.ValueChanged += _ => calculateScale();
-
- autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize);
- autoCursorScale.ValueChanged += _ => calculateScale();
-
- CursorScale = new BindableFloat();
- CursorScale.ValueChanged += e => ActiveCursor.Scale = cursorTrail.Scale = new Vector2(e.NewValue);
-
- calculateScale();
- }
-
- private void calculateScale()
- {
- float scale = userCursorScale.Value;
-
- if (autoCursorScale.Value && beatmap.Value != null)
- {
- // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
- scale *= 1f - 0.7f * (1f + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
- }
-
- CursorScale.Value = scale;
}
protected override void LoadComplete()
@@ -81,6 +61,46 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
base.LoadComplete();
showTrail.BindValueChanged(v => cursorTrail.FadeTo(v.NewValue ? 1 : 0, 200), true);
+
+ userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize);
+ userCursorScale.ValueChanged += _ => calculateScale();
+
+ autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize);
+ autoCursorScale.ValueChanged += _ => calculateScale();
+
+ CursorScale.ValueChanged += e =>
+ {
+ var newScale = new Vector2(e.NewValue);
+
+ ActiveCursor.Scale = newScale;
+ cursorTrail.Scale = newScale;
+ };
+
+ calculateScale();
+ }
+
+ ///
+ /// Get the scale applicable to the ActiveCursor based on a beatmap's circle size.
+ ///
+ public static float GetScaleForCircleSize(float circleSize) =>
+ 1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
+
+ private void calculateScale()
+ {
+ float scale = userCursorScale.Value;
+
+ if (autoCursorScale.Value && beatmap != null)
+ {
+ // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
+ scale *= GetScaleForCircleSize(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
+ }
+
+ CursorScale.Value = scale;
+
+ var newScale = new Vector2(scale);
+
+ ActiveCursor.ScaleTo(newScale, 400, Easing.OutQuint);
+ cursorTrail.Scale = newScale;
}
private int downCount;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs
new file mode 100644
index 0000000000..3c2735ca56
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs
@@ -0,0 +1,97 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online;
+using osu.Game.Online.API;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [TestFixture]
+ public class TestSceneOnlineViewContainer : OsuTestScene
+ {
+ private readonly TestOnlineViewContainer onlineView;
+
+ public TestSceneOnlineViewContainer()
+ {
+ Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = onlineView = new TestOnlineViewContainer()
+ };
+ }
+
+ [Test]
+ public void TestOnlineStateVisibility()
+ {
+ AddStep("set status to online", () => ((DummyAPIAccess)API).State = APIState.Online);
+
+ AddUntilStep("children are visible", () => onlineView.ViewTarget.IsPresent);
+ AddUntilStep("loading animation is not visible", () => !onlineView.LoadingAnimation.IsPresent);
+ }
+
+ [Test]
+ public void TestOfflineStateVisibility()
+ {
+ AddStep("set status to offline", () => ((DummyAPIAccess)API).State = APIState.Offline);
+
+ AddUntilStep("children are not visible", () => !onlineView.ViewTarget.IsPresent);
+ AddUntilStep("loading animation is not visible", () => !onlineView.LoadingAnimation.IsPresent);
+ }
+
+ [Test]
+ public void TestConnectingStateVisibility()
+ {
+ AddStep("set status to connecting", () => ((DummyAPIAccess)API).State = APIState.Connecting);
+
+ AddUntilStep("children are not visible", () => !onlineView.ViewTarget.IsPresent);
+ AddUntilStep("loading animation is visible", () => onlineView.LoadingAnimation.IsPresent);
+ }
+
+ [Test]
+ public void TestFailingStateVisibility()
+ {
+ AddStep("set status to failing", () => ((DummyAPIAccess)API).State = APIState.Failing);
+
+ AddUntilStep("children are not visible", () => !onlineView.ViewTarget.IsPresent);
+ AddUntilStep("loading animation is visible", () => onlineView.LoadingAnimation.IsPresent);
+ }
+
+ private class TestOnlineViewContainer : OnlineViewContainer
+ {
+ public new LoadingAnimation LoadingAnimation => base.LoadingAnimation;
+
+ public CompositeDrawable ViewTarget => base.Content;
+
+ public TestOnlineViewContainer()
+ : base(@"Please sign in to view dummy test content")
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Blue.Opacity(0.8f),
+ },
+ new OsuSpriteText
+ {
+ Text = "dummy online content",
+ Font = OsuFont.Default.With(size: 40),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 6ae3c7ac64..ce959e9057 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.Ruleset, 0, 0, int.MaxValue);
Set(OsuSetting.Skin, 0, -1, int.MaxValue);
- Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details);
+ Set(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
Set(OsuSetting.ShowConvertedBeatmaps, true);
Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1);
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 7f23f9b5d5..a1c3475fd9 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Online.API
public APIState State
{
get => state;
- private set
+ set
{
if (state == value)
return;
diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs
index bd4fedabd4..87c747675a 100644
--- a/osu.Game/Online/Leaderboards/Leaderboard.cs
+++ b/osu.Game/Online/Leaderboards/Leaderboard.cs
@@ -152,7 +152,7 @@ namespace osu.Game.Online.Leaderboards
break;
case PlaceholderState.NotLoggedIn:
- replacePlaceholder(new LoginPlaceholder());
+ replacePlaceholder(new LoginPlaceholder(@"Please sign in to view online leaderboards!"));
break;
case PlaceholderState.NotSupporter:
diff --git a/osu.Game/Online/OnlineViewContainer.cs b/osu.Game/Online/OnlineViewContainer.cs
new file mode 100644
index 0000000000..689c1c0afb
--- /dev/null
+++ b/osu.Game/Online/OnlineViewContainer.cs
@@ -0,0 +1,100 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.Placeholders;
+
+namespace osu.Game.Online
+{
+ ///
+ /// A for displaying online content which require a local user to be logged in.
+ /// Shows its children only when the local user is logged in and supports displaying a placeholder if not.
+ ///
+ public abstract class OnlineViewContainer : Container, IOnlineComponent
+ {
+ protected LoadingAnimation LoadingAnimation { get; private set; }
+
+ protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
+
+ private readonly string placeholderMessage;
+
+ private Placeholder placeholder;
+
+ private const double transform_duration = 300;
+
+ [Resolved]
+ protected IAPIProvider API { get; private set; }
+
+ protected OnlineViewContainer(string placeholderMessage)
+ {
+ this.placeholderMessage = placeholderMessage;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
+ {
+ Content,
+ placeholder = new LoginPlaceholder(placeholderMessage),
+ LoadingAnimation = new LoadingAnimation
+ {
+ Alpha = 0,
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ API.Register(this);
+ }
+
+ public virtual void APIStateChanged(IAPIProvider api, APIState state)
+ {
+ switch (state)
+ {
+ case APIState.Offline:
+ PopContentOut(Content);
+ placeholder.ScaleTo(0.8f).Then().ScaleTo(1, 3 * transform_duration, Easing.OutQuint);
+ placeholder.FadeInFromZero(2 * transform_duration, Easing.OutQuint);
+ LoadingAnimation.Hide();
+ break;
+
+ case APIState.Online:
+ PopContentIn(Content);
+ placeholder.FadeOut(transform_duration / 2, Easing.OutQuint);
+ LoadingAnimation.Hide();
+ break;
+
+ case APIState.Failing:
+ case APIState.Connecting:
+ PopContentOut(Content);
+ LoadingAnimation.Show();
+ placeholder.FadeOut(transform_duration / 2, Easing.OutQuint);
+ break;
+ }
+ }
+
+ ///
+ /// Applies a transform to the online content to make it hidden.
+ ///
+ protected virtual void PopContentOut(Drawable content) => content.FadeOut(transform_duration / 2, Easing.OutQuint);
+
+ ///
+ /// Applies a transform to the online content to make it visible.
+ ///
+ protected virtual void PopContentIn(Drawable content) => content.FadeIn(transform_duration, Easing.OutQuint);
+
+ protected override void Dispose(bool isDisposing)
+ {
+ API?.Unregister(this);
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game/Online/Placeholders/LoginPlaceholder.cs b/osu.Game/Online/Placeholders/LoginPlaceholder.cs
index 591eb976e2..73b0fa27c3 100644
--- a/osu.Game/Online/Placeholders/LoginPlaceholder.cs
+++ b/osu.Game/Online/Placeholders/LoginPlaceholder.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Online.Placeholders
[Resolved(CanBeNull = true)]
private LoginOverlay login { get; set; }
- public LoginPlaceholder()
+ public LoginPlaceholder(string actionMessage)
{
AddIcon(FontAwesome.Solid.UserLock, cp =>
{
@@ -22,7 +22,7 @@ namespace osu.Game.Online.Placeholders
cp.Padding = new MarginPadding { Right = 10 };
});
- AddText(@"Please sign in to view online leaderboards!");
+ AddText(actionMessage);
}
protected override bool OnMouseDown(MouseDownEvent e)
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
index 7ce8a751e0..227eecf9c7 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input.Events;
+using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osuTK;
@@ -30,6 +32,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private float currentZoom = 1;
+ [Resolved(canBeNull: true)]
+ private IFrameBasedClock editorClock { get; set; }
+
public ZoomableScrollContainer()
: base(Direction.Horizontal)
{
@@ -104,8 +109,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override bool OnScroll(ScrollEvent e)
{
if (e.IsPrecise)
+ {
+ // can't handle scroll correctly while playing.
+ // the editor will handle this case for us.
+ if (editorClock?.IsRunning == true)
+ return false;
+
// for now, we don't support zoom when using a precision scroll device. this needs gesture support.
return base.OnScroll(e);
+ }
setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X);
return true;
diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
new file mode 100644
index 0000000000..074341226e
--- /dev/null
+++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
@@ -0,0 +1,180 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Play
+{
+ ///
+ /// Displays beatmap metadata inside
+ ///
+ public class BeatmapMetadataDisplay : Container
+ {
+ private class MetadataLine : Container
+ {
+ public MetadataLine(string left, string right)
+ {
+ AutoSizeAxes = Axes.Both;
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopRight,
+ Margin = new MarginPadding { Right = 5 },
+ Colour = OsuColour.Gray(0.8f),
+ Text = left,
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopLeft,
+ Margin = new MarginPadding { Left = 5 },
+ Text = string.IsNullOrEmpty(right) ? @"-" : right,
+ }
+ };
+ }
+ }
+
+ private readonly WorkingBeatmap beatmap;
+ private readonly Bindable> mods;
+ private readonly Drawable facade;
+ private LoadingAnimation loading;
+ private Sprite backgroundSprite;
+
+ public IBindable> Mods => mods;
+
+ public bool Loading
+ {
+ set
+ {
+ if (value)
+ {
+ loading.Show();
+ backgroundSprite.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint);
+ }
+ else
+ {
+ loading.Hide();
+ backgroundSprite.FadeColour(Color4.White, 400, Easing.OutQuint);
+ }
+ }
+ }
+
+ public BeatmapMetadataDisplay(WorkingBeatmap beatmap, Bindable> mods, Drawable facade)
+ {
+ this.beatmap = beatmap;
+ this.facade = facade;
+
+ this.mods = new Bindable>();
+ this.mods.BindTo(mods);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var metadata = beatmap.BeatmapInfo?.Metadata ?? new BeatmapMetadata();
+
+ AutoSizeAxes = Axes.Both;
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Direction = FillDirection.Vertical,
+ Children = new[]
+ {
+ facade.With(d =>
+ {
+ d.Anchor = Anchor.TopCentre;
+ d.Origin = Anchor.TopCentre;
+ }),
+ new OsuSpriteText
+ {
+ Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
+ Font = OsuFont.GetFont(size: 36, italics: true),
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Margin = new MarginPadding { Top = 15 },
+ },
+ new OsuSpriteText
+ {
+ Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
+ Font = OsuFont.GetFont(size: 26, italics: true),
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ },
+ new Container
+ {
+ Size = new Vector2(300, 60),
+ Margin = new MarginPadding(10),
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ CornerRadius = 10,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ backgroundSprite = new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ Texture = beatmap?.Background,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ FillMode = FillMode.Fill,
+ },
+ loading = new LoadingAnimation { Scale = new Vector2(1.3f) }
+ }
+ },
+ new OsuSpriteText
+ {
+ Text = beatmap?.BeatmapInfo?.Version,
+ Font = OsuFont.GetFont(size: 26, italics: true),
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Margin = new MarginPadding
+ {
+ Bottom = 40
+ },
+ },
+ new MetadataLine("Source", metadata.Source)
+ {
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ },
+ new MetadataLine("Mapper", metadata.AuthorString)
+ {
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ },
+ new ModDisplay
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AutoSizeAxes = Axes.Both,
+ Margin = new MarginPadding { Top = 20 },
+ Current = mods
+ }
+ },
+ }
+ };
+
+ Loading = true;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs
new file mode 100644
index 0000000000..d7f939a883
--- /dev/null
+++ b/osu.Game/Screens/Play/GameplayBeatmap.cs
@@ -0,0 +1,42 @@
+// 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 osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Screens.Play
+{
+ public class GameplayBeatmap : Component, IBeatmap
+ {
+ public readonly IBeatmap PlayableBeatmap;
+
+ public GameplayBeatmap(IBeatmap playableBeatmap)
+ {
+ PlayableBeatmap = playableBeatmap;
+ }
+
+ public BeatmapInfo BeatmapInfo
+ {
+ get => PlayableBeatmap.BeatmapInfo;
+ set => PlayableBeatmap.BeatmapInfo = value;
+ }
+
+ public BeatmapMetadata Metadata => PlayableBeatmap.Metadata;
+
+ public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo;
+
+ public List Breaks => PlayableBeatmap.Breaks;
+
+ public double TotalBreakTime => PlayableBeatmap.TotalBreakTime;
+
+ public IReadOnlyList HitObjects => PlayableBeatmap.HitObjects;
+
+ public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics();
+
+ public IBeatmap Clone() => PlayableBeatmap.Clone();
+ }
+}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index aecd35f7dc..9bfdcd79fe 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -110,6 +110,13 @@ namespace osu.Game.Screens.Play
this.showResults = showResults;
}
+ private GameplayBeatmap gameplayBeatmap;
+
+ private DependencyContainer dependencies;
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ => dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
[BackgroundDependencyLoader]
private void load(AudioManager audio, IAPIProvider api, OsuConfigManager config)
{
@@ -143,6 +150,10 @@ namespace osu.Game.Screens.Play
InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime);
+ AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap));
+
+ dependencies.CacheAs(gameplayBeatmap);
+
addUnderlayComponents(GameplayClockContainer);
addGameplayComponents(GameplayClockContainer, Beatmap.Value);
addOverlayComponents(GameplayClockContainer, Beatmap.Value);
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index f37faac988..01873f7114 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
@@ -12,21 +11,15 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
-using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Framework.Threading;
-using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
-using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Menu;
-using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Users;
using osuTK;
@@ -38,30 +31,58 @@ namespace osu.Game.Screens.Play
{
protected const float BACKGROUND_BLUR = 15;
+ public override bool HideOverlaysOnEnter => hideOverlays;
+
+ public override bool DisallowExternalBeatmapRulesetChanges => true;
+
+ // Here because IsHovered will not update unless we do so.
+ public override bool HandlePositionalInput => true;
+
+ // We show the previous screen status
+ protected override UserActivity InitialActivity => null;
+
+ protected override bool PlayResumeSound => false;
+
+ protected BeatmapMetadataDisplay MetadataInfo;
+
+ protected VisualSettings VisualSettings;
+
+ protected Task LoadTask { get; private set; }
+
+ protected Task DisposalTask { get; private set; }
+
+ private bool backgroundBrightnessReduction;
+
+ protected bool BackgroundBrightnessReduction
+ {
+ set
+ {
+ if (value == backgroundBrightnessReduction)
+ return;
+
+ backgroundBrightnessReduction = value;
+
+ Background.FadeColour(OsuColour.Gray(backgroundBrightnessReduction ? 0.8f : 1), 200);
+ }
+ }
+
+ private bool readyForPush =>
+ player.LoadState == LoadState.Ready && (IsHovered || idleTracker.IsIdle.Value) && inputManager?.DraggedDrawable == null;
+
private readonly Func createPlayer;
private Player player;
private LogoTrackingContainer content;
- protected BeatmapMetadataDisplay MetadataInfo;
-
private bool hideOverlays;
- public override bool HideOverlaysOnEnter => hideOverlays;
-
- protected override UserActivity InitialActivity => null; //shows the previous screen status
-
- public override bool DisallowExternalBeatmapRulesetChanges => true;
-
- protected override bool PlayResumeSound => false;
-
- protected Task LoadTask { get; private set; }
-
- protected Task DisposalTask { get; private set; }
private InputManager inputManager;
+
private IdleTracker idleTracker;
+ private ScheduledDelegate scheduledPushPlayer;
+
[Resolved(CanBeNull = true)]
private NotificationOverlay notificationOverlay { get; set; }
@@ -71,19 +92,11 @@ namespace osu.Game.Screens.Play
[Resolved]
private AudioManager audioManager { get; set; }
- private Bindable muteWarningShownOnce;
-
public PlayerLoader(Func createPlayer)
{
this.createPlayer = createPlayer;
}
- private void restartRequested()
- {
- hideOverlays = true;
- ValidForResume = true;
- }
-
[BackgroundDependencyLoader]
private void load(SessionStatics sessionStatics)
{
@@ -127,11 +140,13 @@ namespace osu.Game.Screens.Play
inputManager = GetContainingInputManager();
}
+ #region Screen handling
+
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
- loadNewPlayer();
+ prepareNewPlayer();
content.ScaleTo(0.7f);
Background?.FadeColour(Color4.White, 800, Easing.OutQuint);
@@ -141,15 +156,7 @@ namespace osu.Game.Screens.Play
MetadataInfo.Delay(750).FadeIn(500);
this.Delay(1800).Schedule(pushWhenLoaded);
- if (!muteWarningShownOnce.Value)
- {
- //Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted.
- if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= audioManager.Volume.MinValue || audioManager.VolumeTrack.Value <= audioManager.VolumeTrack.MinValue)
- {
- notificationOverlay?.Post(new MutedNotification());
- muteWarningShownOnce.Value = true;
- }
- }
+ showMuteWarningIfNeeded();
}
public override void OnResuming(IScreen last)
@@ -160,36 +167,32 @@ namespace osu.Game.Screens.Play
MetadataInfo.Loading = true;
- //we will only be resumed if the player has requested a re-run (see ValidForResume setting above)
- loadNewPlayer();
+ // we will only be resumed if the player has requested a re-run (see restartRequested).
+ prepareNewPlayer();
this.Delay(400).Schedule(pushWhenLoaded);
}
- private void loadNewPlayer()
+ public override void OnSuspending(IScreen next)
{
- var restartCount = player?.RestartCount + 1 ?? 0;
+ base.OnSuspending(next);
- player = createPlayer();
- player.RestartCount = restartCount;
- player.RestartRequested = restartRequested;
+ cancelLoad();
- LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false);
+ BackgroundBrightnessReduction = false;
}
- private void contentIn()
+ public override bool OnExiting(IScreen next)
{
- content.ScaleTo(1, 650, Easing.OutQuint);
- content.FadeInFromZero(400);
- }
+ cancelLoad();
- private void contentOut()
- {
- // Ensure the logo is no longer tracking before we scale the content
- content.StopTracking();
+ content.ScaleTo(0.7f, 150, Easing.InQuint);
+ this.FadeOut(150);
- content.ScaleTo(0.7f, 300, Easing.InQuint);
- content.FadeOut(250);
+ Background.EnableUserDim.Value = false;
+ BackgroundBrightnessReduction = false;
+
+ return base.OnExiting(next);
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
@@ -198,10 +201,7 @@ namespace osu.Game.Screens.Play
const double duration = 300;
- if (!resuming)
- {
- logo.MoveTo(new Vector2(0.5f), duration, Easing.In);
- }
+ if (!resuming) logo.MoveTo(new Vector2(0.5f), duration, Easing.In);
logo.ScaleTo(new Vector2(0.15f), duration, Easing.In);
logo.FadeIn(350);
@@ -219,109 +219,7 @@ namespace osu.Game.Screens.Play
content.StopTracking();
}
- private ScheduledDelegate pushDebounce;
- protected VisualSettings VisualSettings;
-
- // Here because IsHovered will not update unless we do so.
- public override bool HandlePositionalInput => true;
-
- private bool readyForPush => player.LoadState == LoadState.Ready && (IsHovered || idleTracker.IsIdle.Value) && inputManager?.DraggedDrawable == null;
-
- private void pushWhenLoaded()
- {
- if (!this.IsCurrentScreen()) return;
-
- try
- {
- if (!readyForPush)
- {
- // as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce
- // if we become unready for push during the delay.
- cancelLoad();
- return;
- }
-
- if (pushDebounce != null)
- return;
-
- pushDebounce = Scheduler.AddDelayed(() =>
- {
- contentOut();
-
- this.Delay(250).Schedule(() =>
- {
- if (!this.IsCurrentScreen()) return;
-
- LoadTask = null;
-
- //By default, we want to load the player and never be returned to.
- //Note that this may change if the player we load requested a re-run.
- ValidForResume = false;
-
- if (player.LoadedBeatmapSuccessfully)
- this.Push(player);
- else
- this.Exit();
- });
- }, 500);
- }
- finally
- {
- Schedule(pushWhenLoaded);
- }
- }
-
- private void cancelLoad()
- {
- pushDebounce?.Cancel();
- pushDebounce = null;
- }
-
- public override void OnSuspending(IScreen next)
- {
- BackgroundBrightnessReduction = false;
- base.OnSuspending(next);
- cancelLoad();
- }
-
- public override bool OnExiting(IScreen next)
- {
- content.ScaleTo(0.7f, 150, Easing.InQuint);
- this.FadeOut(150);
- cancelLoad();
-
- Background.EnableUserDim.Value = false;
- BackgroundBrightnessReduction = false;
-
- return base.OnExiting(next);
- }
-
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
-
- if (isDisposing)
- {
- // if the player never got pushed, we should explicitly dispose it.
- DisposalTask = LoadTask?.ContinueWith(_ => player.Dispose());
- }
- }
-
- private bool backgroundBrightnessReduction;
-
- protected bool BackgroundBrightnessReduction
- {
- get => backgroundBrightnessReduction;
- set
- {
- if (value == backgroundBrightnessReduction)
- return;
-
- backgroundBrightnessReduction = value;
-
- Background.FadeColour(OsuColour.Gray(backgroundBrightnessReduction ? 0.8f : 1), 200);
- }
- }
+ #endregion
protected override void Update()
{
@@ -350,171 +248,129 @@ namespace osu.Game.Screens.Play
}
}
- protected class BeatmapMetadataDisplay : Container
+ private void prepareNewPlayer()
{
- private class MetadataLine : Container
+ var restartCount = player?.RestartCount + 1 ?? 0;
+
+ player = createPlayer();
+ player.RestartCount = restartCount;
+ player.RestartRequested = restartRequested;
+
+ LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false);
+ }
+
+ private void restartRequested()
+ {
+ hideOverlays = true;
+ ValidForResume = true;
+ }
+
+ private void contentIn()
+ {
+ content.ScaleTo(1, 650, Easing.OutQuint);
+ content.FadeInFromZero(400);
+ }
+
+ private void contentOut()
+ {
+ // Ensure the logo is no longer tracking before we scale the content
+ content.StopTracking();
+
+ content.ScaleTo(0.7f, 300, Easing.InQuint);
+ content.FadeOut(250);
+ }
+
+ private void pushWhenLoaded()
+ {
+ if (!this.IsCurrentScreen()) return;
+
+ try
{
- public MetadataLine(string left, string right)
+ if (!readyForPush)
{
- AutoSizeAxes = Axes.Both;
- Children = new Drawable[]
- {
- new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopRight,
- Margin = new MarginPadding { Right = 5 },
- Colour = OsuColour.Gray(0.8f),
- Text = left,
- },
- new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopLeft,
- Margin = new MarginPadding { Left = 5 },
- Text = string.IsNullOrEmpty(right) ? @"-" : right,
- }
- };
+ // as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce
+ // if we become unready for push during the delay.
+ cancelLoad();
+ return;
}
- }
- private readonly WorkingBeatmap beatmap;
- private readonly Bindable> mods;
- private readonly Drawable facade;
- private LoadingAnimation loading;
- private Sprite backgroundSprite;
+ if (scheduledPushPlayer != null)
+ return;
- public IBindable> Mods => mods;
-
- public bool Loading
- {
- set
+ scheduledPushPlayer = Scheduler.AddDelayed(() =>
{
- if (value)
+ contentOut();
+
+ this.Delay(250).Schedule(() =>
{
- loading.Show();
- backgroundSprite.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint);
- }
- else
- {
- loading.Hide();
- backgroundSprite.FadeColour(Color4.White, 400, Easing.OutQuint);
- }
+ if (!this.IsCurrentScreen()) return;
+
+ LoadTask = null;
+
+ //By default, we want to load the player and never be returned to.
+ //Note that this may change if the player we load requested a re-run.
+ ValidForResume = false;
+
+ if (player.LoadedBeatmapSuccessfully)
+ this.Push(player);
+ else
+ this.Exit();
+ });
+ }, 500);
+ }
+ finally
+ {
+ Schedule(pushWhenLoaded);
+ }
+ }
+
+ private void cancelLoad()
+ {
+ scheduledPushPlayer?.Cancel();
+ scheduledPushPlayer = null;
+ }
+
+ #region Disposal
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (isDisposing)
+ {
+ // if the player never got pushed, we should explicitly dispose it.
+ DisposalTask = LoadTask?.ContinueWith(_ => player.Dispose());
+ }
+ }
+
+ #endregion
+
+ #region Mute warning
+
+ private Bindable muteWarningShownOnce;
+
+ private void showMuteWarningIfNeeded()
+ {
+ if (!muteWarningShownOnce.Value)
+ {
+ //Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted.
+ if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= audioManager.Volume.MinValue || audioManager.VolumeTrack.Value <= audioManager.VolumeTrack.MinValue)
+ {
+ notificationOverlay?.Post(new MutedNotification());
+ muteWarningShownOnce.Value = true;
}
}
-
- public BeatmapMetadataDisplay(WorkingBeatmap beatmap, Bindable> mods, Drawable facade)
- {
- this.beatmap = beatmap;
- this.facade = facade;
-
- this.mods = new Bindable>();
- this.mods.BindTo(mods);
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- var metadata = beatmap.BeatmapInfo?.Metadata ?? new BeatmapMetadata();
-
- AutoSizeAxes = Axes.Both;
- Children = new Drawable[]
- {
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Direction = FillDirection.Vertical,
- Children = new[]
- {
- facade.With(d =>
- {
- d.Anchor = Anchor.TopCentre;
- d.Origin = Anchor.TopCentre;
- }),
- new OsuSpriteText
- {
- Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
- Font = OsuFont.GetFont(size: 36, italics: true),
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Margin = new MarginPadding { Top = 15 },
- },
- new OsuSpriteText
- {
- Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
- Font = OsuFont.GetFont(size: 26, italics: true),
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- },
- new Container
- {
- Size = new Vector2(300, 60),
- Margin = new MarginPadding(10),
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- CornerRadius = 10,
- Masking = true,
- Children = new Drawable[]
- {
- backgroundSprite = new Sprite
- {
- RelativeSizeAxes = Axes.Both,
- Texture = beatmap?.Background,
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- FillMode = FillMode.Fill,
- },
- loading = new LoadingAnimation { Scale = new Vector2(1.3f) }
- }
- },
- new OsuSpriteText
- {
- Text = beatmap?.BeatmapInfo?.Version,
- Font = OsuFont.GetFont(size: 26, italics: true),
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Margin = new MarginPadding
- {
- Bottom = 40
- },
- },
- new MetadataLine("Source", metadata.Source)
- {
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- },
- new MetadataLine("Mapper", metadata.AuthorString)
- {
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- },
- new ModDisplay
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Top = 20 },
- Current = mods
- }
- },
- }
- };
-
- Loading = true;
- }
}
private class MutedNotification : SimpleNotification
{
+ public override bool IsImportant => true;
+
public MutedNotification()
{
Text = "Your music volume is set to 0%! Click here to restore it.";
}
- public override bool IsImportant => true;
-
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audioManager, NotificationOverlay notificationOverlay, VolumeOverlay volumeOverlay)
{
@@ -533,5 +389,7 @@ namespace osu.Game.Screens.Play
};
}
}
+
+ #endregion
}
}
diff --git a/osu.Game/Screens/Select/BeatmapDetailArea.cs b/osu.Game/Screens/Select/BeatmapDetailArea.cs
index 71733c9f06..2e78b1aed2 100644
--- a/osu.Game/Screens/Select/BeatmapDetailArea.cs
+++ b/osu.Game/Screens/Select/BeatmapDetailArea.cs
@@ -2,37 +2,40 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
-using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Screens.Select
{
- public class BeatmapDetailArea : Container
+ public abstract class BeatmapDetailArea : Container
{
private const float details_padding = 10;
- private readonly Container content;
- protected override Container Content => content;
-
- public readonly BeatmapDetails Details;
- public readonly BeatmapLeaderboard Leaderboard;
-
private WorkingBeatmap beatmap;
- public WorkingBeatmap Beatmap
+ public virtual WorkingBeatmap Beatmap
{
get => beatmap;
set
{
beatmap = value;
- Details.Beatmap = beatmap?.BeatmapInfo;
- Leaderboard.Beatmap = beatmap is DummyWorkingBeatmap ? null : beatmap?.BeatmapInfo;
+
+ Details.Beatmap = value?.BeatmapInfo;
}
}
- public BeatmapDetailArea()
+ public readonly BeatmapDetails Details;
+
+ protected Bindable CurrentTab => tabControl.Current;
+
+ private readonly Container content;
+ protected override Container Content => content;
+
+ private readonly BeatmapDetailAreaTabControl tabControl;
+
+ protected BeatmapDetailArea()
{
AddRangeInternal(new Drawable[]
{
@@ -40,51 +43,62 @@ namespace osu.Game.Screens.Select
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = BeatmapDetailAreaTabControl.HEIGHT },
- },
- new BeatmapDetailAreaTabControl
- {
- RelativeSizeAxes = Axes.X,
- OnFilter = (tab, mods) =>
+ Child = Details = new BeatmapDetails
{
- Leaderboard.FilterMods = mods;
-
- switch (tab)
- {
- case BeatmapDetailTab.Details:
- Details.Show();
- Leaderboard.Hide();
- break;
-
- default:
- Details.Hide();
- Leaderboard.Scope = (BeatmapLeaderboardScope)tab - 1;
- Leaderboard.Show();
- break;
- }
- },
+ RelativeSizeAxes = Axes.X,
+ Alpha = 0,
+ Margin = new MarginPadding { Top = details_padding },
+ }
},
- });
-
- AddRange(new Drawable[]
- {
- Details = new BeatmapDetails
+ tabControl = new BeatmapDetailAreaTabControl
{
RelativeSizeAxes = Axes.X,
- Alpha = 0,
- Margin = new MarginPadding { Top = details_padding },
+ TabItems = CreateTabItems(),
+ OnFilter = OnTabChanged,
},
- Leaderboard = new BeatmapLeaderboard
- {
- RelativeSizeAxes = Axes.Both,
- }
});
}
+ ///
+ /// Refreshes the currently-displayed details.
+ ///
+ public virtual void Refresh()
+ {
+ }
+
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
Details.Height = Math.Min(DrawHeight - details_padding * 3 - BeatmapDetailAreaTabControl.HEIGHT, 450);
}
+
+ ///
+ /// Invoked when a new tab is selected.
+ ///
+ /// The tab that was selected.
+ /// Whether the currently-selected mods should be considered.
+ protected virtual void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods)
+ {
+ switch (tab)
+ {
+ case BeatmapDetailAreaDetailTabItem _:
+ Details.Show();
+ break;
+
+ default:
+ Details.Hide();
+ break;
+ }
+ }
+
+ ///
+ /// Creates the tabs to be displayed.
+ ///
+ /// The tabs.
+ protected virtual BeatmapDetailAreaTabItem[] CreateTabItems() => new BeatmapDetailAreaTabItem[]
+ {
+ new BeatmapDetailAreaDetailTabItem(),
+ };
}
}
diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs b/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs
new file mode 100644
index 0000000000..7376cb4708
--- /dev/null
+++ b/osu.Game/Screens/Select/BeatmapDetailAreaDetailTabItem.cs
@@ -0,0 +1,10 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Screens.Select
+{
+ public class BeatmapDetailAreaDetailTabItem : BeatmapDetailAreaTabItem
+ {
+ public override string Name => "Details";
+ }
+}
diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs b/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs
new file mode 100644
index 0000000000..066944e9d2
--- /dev/null
+++ b/osu.Game/Screens/Select/BeatmapDetailAreaLeaderboardTabItem.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
+namespace osu.Game.Screens.Select
+{
+ public class BeatmapDetailAreaLeaderboardTabItem : BeatmapDetailAreaTabItem
+ where TScope : Enum
+ {
+ public override string Name => Scope.ToString();
+
+ public override bool FilterableByMods => true;
+
+ public readonly TScope Scope;
+
+ public BeatmapDetailAreaLeaderboardTabItem(TScope scope)
+ {
+ Scope = scope;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs
index 19ecdb6dbf..f4bf1ab059 100644
--- a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs
+++ b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -18,14 +19,25 @@ namespace osu.Game.Screens.Select
public class BeatmapDetailAreaTabControl : Container
{
public const float HEIGHT = 24;
+
+ public Bindable Current
+ {
+ get => tabs.Current;
+ set => tabs.Current = value;
+ }
+
+ public Action OnFilter; //passed the selected tab and if mods is checked
+
+ public IReadOnlyList TabItems
+ {
+ get => tabs.Items;
+ set => tabs.Items = value;
+ }
+
private readonly OsuTabControlCheckbox modsCheckbox;
- private readonly OsuTabControl tabs;
+ private readonly OsuTabControl tabs;
private readonly Container tabsContainer;
- public Action OnFilter; //passed the selected tab and if mods is checked
-
- private Bindable selectedTab;
-
public BeatmapDetailAreaTabControl()
{
Height = HEIGHT;
@@ -43,7 +55,7 @@ namespace osu.Game.Screens.Select
tabsContainer = new Container
{
RelativeSizeAxes = Axes.Both,
- Child = tabs = new OsuTabControl
+ Child = tabs = new OsuTabControl
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
@@ -68,29 +80,22 @@ namespace osu.Game.Screens.Select
private void load(OsuColour colour, OsuConfigManager config)
{
modsCheckbox.AccentColour = tabs.AccentColour = colour.YellowLight;
-
- selectedTab = config.GetBindable(OsuSetting.BeatmapDetailTab);
-
- tabs.Current.BindTo(selectedTab);
- tabs.Current.TriggerChange();
}
private void invokeOnFilter()
{
OnFilter?.Invoke(tabs.Current.Value, modsCheckbox.Current.Value);
- modsCheckbox.FadeTo(tabs.Current.Value == BeatmapDetailTab.Details ? 0 : 1, 200, Easing.OutQuint);
-
- tabsContainer.Padding = new MarginPadding { Right = tabs.Current.Value == BeatmapDetailTab.Details ? 0 : 100 };
+ if (tabs.Current.Value.FilterableByMods)
+ {
+ modsCheckbox.FadeTo(1, 200, Easing.OutQuint);
+ tabsContainer.Padding = new MarginPadding { Right = 100 };
+ }
+ else
+ {
+ modsCheckbox.FadeTo(0, 200, Easing.OutQuint);
+ tabsContainer.Padding = new MarginPadding();
+ }
}
}
-
- public enum BeatmapDetailTab
- {
- Details,
- Local,
- Country,
- Global,
- Friends
- }
}
diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaTabItem.cs b/osu.Game/Screens/Select/BeatmapDetailAreaTabItem.cs
new file mode 100644
index 0000000000..f28e5a7c22
--- /dev/null
+++ b/osu.Game/Screens/Select/BeatmapDetailAreaTabItem.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
+namespace osu.Game.Screens.Select
+{
+ public abstract class BeatmapDetailAreaTabItem : IEquatable
+ {
+ ///
+ /// The name of this tab, to be displayed in the tab control.
+ ///
+ public abstract string Name { get; }
+
+ ///
+ /// Whether the contents of this tab can be filtered by the user's currently-selected mods.
+ ///
+ public virtual bool FilterableByMods => false;
+
+ public override string ToString() => Name;
+
+ public bool Equals(BeatmapDetailAreaTabItem other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+
+ return Name == other.Name;
+ }
+
+ public override int GetHashCode()
+ {
+ return Name != null ? Name.GetHashCode() : 0;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs
index 251456bf0d..826677ee30 100644
--- a/osu.Game/Screens/Select/MatchSongSelect.cs
+++ b/osu.Game/Screens/Select/MatchSongSelect.cs
@@ -35,6 +35,8 @@ namespace osu.Game.Screens.Select
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
}
+ protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); // Todo: Temporary
+
protected override bool OnStart()
{
var item = new PlaylistItem
diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs
new file mode 100644
index 0000000000..d719502a4f
--- /dev/null
+++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs
@@ -0,0 +1,143 @@
+// 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.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Screens.Select.Leaderboards;
+
+namespace osu.Game.Screens.Select
+{
+ public class PlayBeatmapDetailArea : BeatmapDetailArea
+ {
+ public readonly BeatmapLeaderboard Leaderboard;
+
+ public override WorkingBeatmap Beatmap
+ {
+ get => base.Beatmap;
+ set
+ {
+ base.Beatmap = value;
+
+ Leaderboard.Beatmap = value is DummyWorkingBeatmap ? null : value?.BeatmapInfo;
+ }
+ }
+
+ private Bindable selectedTab;
+
+ public PlayBeatmapDetailArea()
+ {
+ Add(Leaderboard = new BeatmapLeaderboard { RelativeSizeAxes = Axes.Both });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ selectedTab = config.GetBindable(OsuSetting.BeatmapDetailTab);
+ selectedTab.BindValueChanged(tab => CurrentTab.Value = getTabItemFromTabType(tab.NewValue), true);
+ CurrentTab.BindValueChanged(tab => selectedTab.Value = getTabTypeFromTabItem(tab.NewValue));
+ }
+
+ public override void Refresh()
+ {
+ base.Refresh();
+
+ Leaderboard.RefreshScores();
+ }
+
+ protected override void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods)
+ {
+ base.OnTabChanged(tab, selectedMods);
+
+ Leaderboard.FilterMods = selectedMods;
+
+ switch (tab)
+ {
+ case BeatmapDetailAreaLeaderboardTabItem leaderboard:
+ Leaderboard.Scope = leaderboard.Scope;
+ Leaderboard.Show();
+ break;
+
+ default:
+ Leaderboard.Hide();
+ break;
+ }
+ }
+
+ protected override BeatmapDetailAreaTabItem[] CreateTabItems() => base.CreateTabItems().Concat(new BeatmapDetailAreaTabItem[]
+ {
+ new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local),
+ new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country),
+ new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Global),
+ new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend),
+ }).ToArray();
+
+ private BeatmapDetailAreaTabItem getTabItemFromTabType(TabType type)
+ {
+ switch (type)
+ {
+ case TabType.Details:
+ return new BeatmapDetailAreaDetailTabItem();
+
+ case TabType.Local:
+ return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Local);
+
+ case TabType.Country:
+ return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Country);
+
+ case TabType.Global:
+ return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Global);
+
+ case TabType.Friends:
+ return new BeatmapDetailAreaLeaderboardTabItem(BeatmapLeaderboardScope.Friend);
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(type));
+ }
+ }
+
+ private TabType getTabTypeFromTabItem(BeatmapDetailAreaTabItem item)
+ {
+ switch (item)
+ {
+ case BeatmapDetailAreaDetailTabItem _:
+ return TabType.Details;
+
+ case BeatmapDetailAreaLeaderboardTabItem leaderboardTab:
+ switch (leaderboardTab.Scope)
+ {
+ case BeatmapLeaderboardScope.Local:
+ return TabType.Local;
+
+ case BeatmapLeaderboardScope.Country:
+ return TabType.Country;
+
+ case BeatmapLeaderboardScope.Global:
+ return TabType.Global;
+
+ case BeatmapLeaderboardScope.Friend:
+ return TabType.Friends;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(item));
+ }
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(item));
+ }
+ }
+
+ public enum TabType
+ {
+ Details,
+ Local,
+ Country,
+ Global,
+ Friends
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs
index f1dd125362..e744fd6a7b 100644
--- a/osu.Game/Screens/Select/PlaySongSelect.cs
+++ b/osu.Game/Screens/Select/PlaySongSelect.cs
@@ -29,8 +29,12 @@ namespace osu.Game.Screens.Select
ValidForResume = false;
Edit();
}, Key.Number4);
+
+ ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += score => this.Push(new SoloResults(score));
}
+ protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
+
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 0da260d752..67626d1e4f 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -23,7 +23,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
-using osu.Game.Screens.Play;
using osu.Game.Screens.Select.Options;
using osu.Game.Skinning;
using osuTK;
@@ -207,11 +206,11 @@ namespace osu.Game.Screens.Select
Left = left_area_padding,
Right = left_area_padding * 2,
},
- Child = BeatmapDetails = new BeatmapDetailArea
+ Child = BeatmapDetails = CreateBeatmapDetailArea().With(d =>
{
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Top = 10, Right = 5 },
- },
+ d.RelativeSizeAxes = Axes.Both;
+ d.Padding = new MarginPadding { Top = 10, Right = 5 };
+ })
},
}
},
@@ -262,8 +261,6 @@ namespace osu.Game.Screens.Select
});
}
- BeatmapDetails.Leaderboard.ScoreSelected += score => this.Push(new SoloResults(score));
-
if (Footer != null)
{
Footer.AddButton(new FooterButtonMods { Current = Mods }, ModSelect);
@@ -319,6 +316,11 @@ namespace osu.Game.Screens.Select
return dependencies;
}
+ ///
+ /// Creates the beatmap details to be displayed underneath the wedge.
+ ///
+ protected abstract BeatmapDetailArea CreateBeatmapDetailArea();
+
public void Edit(BeatmapInfo beatmap = null)
{
if (!AllowEditing)
@@ -533,7 +535,7 @@ namespace osu.Game.Screens.Select
Carousel.AllowSelection = true;
- BeatmapDetails.Leaderboard.RefreshScores();
+ BeatmapDetails.Refresh();
Beatmap.Value.Track.Looping = true;
music?.ResetTrackAdjustments();
@@ -716,7 +718,7 @@ namespace osu.Game.Screens.Select
dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmap, () =>
// schedule done here rather than inside the dialog as the dialog may fade out and never callback.
- Schedule(() => BeatmapDetails.Leaderboard.RefreshScores())));
+ Schedule(() => BeatmapDetails.Refresh())));
}
public virtual bool OnPressed(GlobalAction action)