diff --git a/osu.Android.props b/osu.Android.props
index bff3627af7..85857771a5 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
index 9959b24b35..cc4337fb02 100644
--- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
+++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs
@@ -76,7 +76,7 @@ namespace osu.Desktop.Security
private void load(OsuColour colours)
{
Icon = FontAwesome.Solid.ShieldAlt;
- IconBackground.Colour = colours.YellowDark;
+ IconContent.Colour = colours.YellowDark;
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
index 15b38b39ba..0354228cca 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Database;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
@@ -34,10 +35,14 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
AddStep("setup compose screen", () =>
{
- var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
+ var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 4 })
{
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
- });
+ };
+
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
+
+ var editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null));
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
@@ -50,7 +55,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
(typeof(IBeatSnapProvider), editorBeatmap),
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
},
- Child = new ComposeScreen { State = { Value = Visibility.Visible } },
+ Children = new Drawable[]
+ {
+ editorBeatmap,
+ new ComposeScreen { State = { Value = Visibility.Visible } },
+ }
};
});
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index a156726f94..c39d61020c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (maxStrain == 0)
return 0;
- return objectStrains.Aggregate((total, next) => total + (1.0 / (1.0 + Math.Exp(-(next / maxStrain * 12.0 - 6.0)))));
+ return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
index b48f0b4ccd..5e2a92e5e9 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods
};
[SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")]
- public BindableBool Metronome { get; } = new BindableBool(true);
+ public Bindable Metronome { get; } = new BindableBool(true);
#region Constants
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index a5468ff613..e3c1b1e168 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -14,6 +14,7 @@ using osu.Framework.Caching;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
@@ -165,11 +166,15 @@ namespace osu.Game.Rulesets.Osu.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
+#pragma warning disable 618
+ var legacyDifficultyPoint = DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint;
+#pragma warning restore 618
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
+ bool generateTicks = legacyDifficultyPoint?.GenerateTicks ?? true;
Velocity = scoringDistance / timingPoint.BeatLength;
- TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
+ TickDistance = generateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 9acd3a6cab..9fc1eb7650 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -919,5 +919,30 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints[1].Position, Is.Not.EqualTo(Vector2.Zero));
}
}
+
+ [Test]
+ public void TestNaNControlPoints()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("nan-control-points.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
+
+ Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(1));
+ Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(2));
+
+ Assert.That(controlPoints.TimingPointAt(1000).BeatLength, Is.EqualTo(500));
+
+ Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1));
+ Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1));
+
+#pragma warning disable 618
+ Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False);
+ Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True);
+#pragma warning restore 618
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs b/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs
index f3673b0e7f..339063633a 100644
--- a/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs
@@ -14,7 +14,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
public class ParsingTest
{
[Test]
- public void TestNaNHandling() => allThrow("NaN");
+ public void TestNaNHandling()
+ {
+ allThrow("NaN");
+ Assert.That(Parsing.ParseFloat("NaN", allowNaN: true), Is.NaN);
+ Assert.That(Parsing.ParseDouble("NaN", allowNaN: true), Is.NaN);
+ }
[Test]
public void TestBadStringHandling() => allThrow("Random string 123");
diff --git a/osu.Game.Tests/Resources/nan-control-points.osu b/osu.Game.Tests/Resources/nan-control-points.osu
new file mode 100644
index 0000000000..dcaa705116
--- /dev/null
+++ b/osu.Game.Tests/Resources/nan-control-points.osu
@@ -0,0 +1,15 @@
+osu file format v14
+
+[TimingPoints]
+
+// NaN bpm (should be rejected)
+0,NaN,4,2,0,100,1,0
+
+// 120 bpm
+1000,500,4,2,0,100,1,0
+
+// NaN slider velocity
+2000,NaN,4,3,0,100,0,1
+
+// 1.0x slider velocity
+3000,-100,4,3,0,100,0,1
\ No newline at end of file
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
index 291630fa3a..0df44b9ac4 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
@@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
@@ -34,9 +35,11 @@ namespace osu.Game.Tests.Visual.Editing
{
var beatmap = new OsuBeatmap
{
- BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
+ BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
};
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
+
editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null));
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
@@ -50,7 +53,11 @@ namespace osu.Game.Tests.Visual.Editing
(typeof(IBeatSnapProvider), editorBeatmap),
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
},
- Child = new ComposeScreen { State = { Value = Visibility.Visible } },
+ Children = new Drawable[]
+ {
+ editorBeatmap,
+ new ComposeScreen { State = { Value = Visibility.Visible } },
+ }
};
});
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index cad8c62233..a6abdd7ee1 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Player.OnUpdate += _ =>
{
double currentTime = Player.GameplayClockContainer.CurrentTime;
- alwaysGoingForward &= currentTime >= lastTime;
+ alwaysGoingForward &= currentTime >= lastTime - 500;
lastTime = currentTime;
};
});
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
resumeAndConfirm();
- AddAssert("time didn't go backwards", () => alwaysGoingForward);
+ AddAssert("time didn't go too far backwards", () => alwaysGoingForward);
AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0));
}
@@ -90,6 +90,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("player not playing", () => !Player.LocalUserPlaying.Value);
resumeAndConfirm();
+
+ AddAssert("Resumed without seeking forward", () => Player.LastResumeTime, () => Is.LessThanOrEqualTo(Player.LastPauseTime));
+
AddUntilStep("player playing", () => Player.LocalUserPlaying.Value);
}
@@ -378,7 +381,16 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown);
private void confirmClockRunning(bool isRunning) =>
- AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.IsRunning == isRunning);
+ AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () =>
+ {
+ bool completed = Player.GameplayClockContainer.IsRunning == isRunning;
+
+ if (completed)
+ {
+ }
+
+ return completed;
+ });
protected override bool AllowFail => true;
@@ -386,6 +398,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected class PausePlayer : TestPlayer
{
+ public double LastPauseTime { get; private set; }
+ public double LastResumeTime { get; private set; }
+
public bool FailOverlayVisible => FailOverlay.State.Value == Visibility.Visible;
public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible;
@@ -399,6 +414,23 @@ namespace osu.Game.Tests.Visual.Gameplay
base.OnEntering(e);
GameplayClockContainer.Stop();
}
+
+ private bool? isRunning;
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ if (GameplayClockContainer.IsRunning != isRunning)
+ {
+ isRunning = GameplayClockContainer.IsRunning;
+
+ if (isRunning.Value)
+ LastResumeTime = GameplayClockContainer.CurrentTime;
+ else
+ LastPauseTime = GameplayClockContainer.CurrentTime;
+ }
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 922529cf19..b6c17fbaca 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -14,11 +14,10 @@ using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
-using osu.Framework.Utils;
using osu.Framework.Screens;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Configuration;
-using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
@@ -30,7 +29,6 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Utils;
using osuTK.Input;
-using SkipOverlay = osu.Game.Screens.Play.SkipOverlay;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -83,6 +81,20 @@ namespace osu.Game.Tests.Visual.Gameplay
[SetUp]
public void Setup() => Schedule(() => player = null);
+ [SetUpSteps]
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("read all notifications", () =>
+ {
+ notificationOverlay.Show();
+ notificationOverlay.Hide();
+ });
+
+ AddUntilStep("wait for no notifications", () => notificationOverlay.UnreadCount.Value, () => Is.EqualTo(0));
+ }
+
///
/// Sets the input manager child to a new test player loader container instance.
///
@@ -287,16 +299,9 @@ namespace osu.Game.Tests.Visual.Gameplay
saveVolumes();
- AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1);
- AddStep("click notification", () =>
- {
- var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last();
- var flowContainer = scrollContainer.Children.OfType>().First();
- var notification = flowContainer.First();
+ AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value, () => Is.EqualTo(1));
- InputManager.MoveMouseTo(notification);
- InputManager.Click(MouseButton.Left);
- });
+ clickNotificationIfAny();
AddAssert("check " + volumeName, assert);
@@ -366,15 +371,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}));
AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready);
AddAssert($"notification {(shouldWarn ? "triggered" : "not triggered")}", () => notificationOverlay.UnreadCount.Value == (shouldWarn ? 1 : 0));
- AddStep("click notification", () =>
- {
- var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last();
- var flowContainer = scrollContainer.Children.OfType>().First();
- var notification = flowContainer.First();
-
- InputManager.MoveMouseTo(notification);
- InputManager.Click(MouseButton.Left);
- });
+ clickNotificationIfAny();
AddUntilStep("wait for player load", () => player.IsLoaded);
}
@@ -439,6 +436,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("skip button not visible", () => !checkSkipButtonVisible());
}
+ private void clickNotificationIfAny()
+ {
+ AddStep("click notification", () => notificationOverlay.ChildrenOfType().FirstOrDefault()?.TriggerClick());
+ }
+
private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault();
private class TestPlayerLoader : PlayerLoader
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs
index b6b3650c83..6b02449aa3 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs
@@ -27,8 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private const double skip_time = 6000;
- [SetUp]
- public void SetUp() => Schedule(() =>
+ private void createTest(double skipTime = skip_time) => AddStep("create test", () =>
{
requestCount = 0;
increment = skip_time;
@@ -40,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- skip = new TestSkipOverlay(skip_time)
+ skip = new TestSkipOverlay(skipTime)
{
RequestSkip = () =>
{
@@ -55,9 +54,25 @@ namespace osu.Game.Tests.Visual.Gameplay
gameplayClock = gameplayClockContainer;
});
+ [Test]
+ public void TestSkipTimeZero()
+ {
+ createTest(0);
+ AddUntilStep("wait for skip overlay expired", () => !skip.IsAlive);
+ }
+
+ [Test]
+ public void TestSkipTimeEqualToSkip()
+ {
+ createTest(MasterGameplayClockContainer.MINIMUM_SKIP_TIME);
+ AddUntilStep("wait for skip overlay expired", () => !skip.IsAlive);
+ }
+
[Test]
public void TestFadeOnIdle()
{
+ createTest();
+
AddStep("move mouse", () => InputManager.MoveMouseTo(Vector2.Zero));
AddUntilStep("fully visible", () => skip.FadingContent.Alpha == 1);
AddUntilStep("wait for fade", () => skip.FadingContent.Alpha < 1);
@@ -70,6 +85,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestClickableAfterFade()
{
+ createTest();
+
AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for fade", () => skip.FadingContent.Alpha == 0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
@@ -79,6 +96,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestClickOnlyActuatesOnce()
{
+ createTest();
+
AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre));
AddStep("click", () =>
{
@@ -94,6 +113,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestClickOnlyActuatesMultipleTimes()
{
+ createTest();
+
AddStep("set increment lower", () => increment = 3000);
AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre));
AddStep("click", () => InputManager.Click(MouseButton.Left));
@@ -106,6 +127,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestDoesntFadeOnMouseDown()
{
+ createTest();
+
AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre));
AddStep("button down", () => InputManager.PressButton(MouseButton.Left));
AddUntilStep("wait for overlay disappear", () => !skip.OverlayContent.IsPresent);
diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
index 66e3612113..e0f0df0554 100644
--- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
+++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
@@ -52,6 +52,7 @@ namespace osu.Game.Tests.Visual.Menus
},
notifications = new NotificationOverlay
{
+ Depth = float.MinValue,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}
@@ -82,7 +83,14 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
public virtual void TestPlayIntroWithFailingAudioDevice()
{
- AddStep("hide notifications", () => notifications.Hide());
+ AddStep("reset notifications", () =>
+ {
+ notifications.Show();
+ notifications.Hide();
+ });
+
+ AddUntilStep("wait for no notifications", () => notifications.UnreadCount.Value, () => Is.EqualTo(0));
+
AddStep("restart sequence", () =>
{
logo.FinishTransforms();
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs
index f4078676c9..fe26d59812 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs
@@ -26,16 +26,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestImportantNotificationDoesntInterruptSetup()
{
AddStep("post important notification", () => Game.Notifications.Post(new SimpleNotification { Text = "Important notification" }));
- AddAssert("no notification posted", () => Game.Notifications.UnreadCount.Value == 0);
AddAssert("first-run setup still visible", () => Game.FirstRunOverlay.State.Value == Visibility.Visible);
-
- AddUntilStep("finish first-run setup", () =>
- {
- Game.FirstRunOverlay.NextButton.TriggerClick();
- return Game.FirstRunOverlay.State.Value == Visibility.Hidden;
- });
- AddWaitStep("wait for post delay", 5);
- AddAssert("notifications shown", () => Game.Notifications.State.Value == Visibility.Visible);
AddAssert("notification posted", () => Game.Notifications.UnreadCount.Value == 1);
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
index b845b85e1f..16d564f0ee 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs
@@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("notification arrived", () => notificationOverlay.Verify(n => n.Post(It.IsAny()), Times.Once));
- AddStep("run notification action", () => lastNotification.Activated());
+ AddStep("run notification action", () => lastNotification.Activated?.Invoke());
AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible);
AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
index c196b204b9..38eecaa052 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
@@ -110,7 +110,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep(@"simple #1", sendHelloNotification);
- AddAssert("Is visible", () => notificationOverlay.State.Value == Visibility.Visible);
+ AddAssert("toast displayed", () => notificationOverlay.ToastCount == 1);
+ AddAssert("is not visible", () => notificationOverlay.State.Value == Visibility.Hidden);
checkDisplayedCount(1);
@@ -183,7 +184,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
private void checkDisplayedCount(int expected) =>
- AddAssert($"Displayed count is {expected}", () => notificationOverlay.UnreadCount.Value == expected);
+ AddUntilStep($"Displayed count is {expected}", () => notificationOverlay.UnreadCount.Value == expected);
private void sendDownloadProgress()
{
diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs
index 0f73b60774..6b64a1156e 100644
--- a/osu.Game.Tournament/Models/LadderInfo.cs
+++ b/osu.Game.Tournament/Models/LadderInfo.cs
@@ -40,5 +40,7 @@ namespace osu.Game.Tournament.Models
MinValue = 3,
MaxValue = 4,
};
+
+ public Bindable AutoProgressScreens = new BindableBool(true);
}
}
diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
index 54ae4c0366..8a23ee65da 100644
--- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
@@ -199,16 +199,19 @@ namespace osu.Game.Tournament.Screens.Gameplay
case TourneyState.Idle:
contract();
- const float delay_before_progression = 4000;
-
- // if we've returned to idle and the last screen was ranking
- // we should automatically proceed after a short delay
- if (lastState == TourneyState.Ranking && !warmup.Value)
+ if (LadderInfo.AutoProgressScreens.Value)
{
- if (CurrentMatch.Value?.Completed.Value == true)
- scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression);
- else if (CurrentMatch.Value?.Completed.Value == false)
- scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression);
+ const float delay_before_progression = 4000;
+
+ // if we've returned to idle and the last screen was ranking
+ // we should automatically proceed after a short delay
+ if (lastState == TourneyState.Ranking && !warmup.Value)
+ {
+ if (CurrentMatch.Value?.Completed.Value == true)
+ scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression);
+ else if (CurrentMatch.Value?.Completed.Value == false)
+ scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression);
+ }
}
break;
diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
index 5eb2142fae..4d36515316 100644
--- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
+++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
@@ -197,10 +197,13 @@ namespace osu.Game.Tournament.Screens.MapPool
setNextMode();
- if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick))
+ if (LadderInfo.AutoProgressScreens.Value)
{
- scheduledChange?.Cancel();
- scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000);
+ if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick))
+ {
+ scheduledChange?.Cancel();
+ scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000);
+ }
}
}
diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
index 2b2dce3664..ff781dec80 100644
--- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
+++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
@@ -131,6 +131,12 @@ namespace osu.Game.Tournament.Screens.Setup
windowSize.Value = new Size((int)(height * aspect_ratio / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), height);
}
},
+ new LabelledSwitchButton
+ {
+ Label = "Auto advance screens",
+ Description = "Screens will progress automatically from gameplay -> results -> map pool",
+ Current = LadderInfo.AutoProgressScreens,
+ },
};
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 89d3465ab6..75500fbc4e 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -373,7 +373,11 @@ namespace osu.Game.Beatmaps.Formats
string[] split = line.Split(',');
double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim()));
- double beatLength = Parsing.ParseDouble(split[1].Trim());
+
+ // beatLength is allowed to be NaN to handle an edge case in which some beatmaps use NaN slider velocity to disable slider tick generation (see LegacyDifficultyControlPoint).
+ double beatLength = Parsing.ParseDouble(split[1].Trim(), allowNaN: true);
+
+ // If beatLength is NaN, speedMultiplier should still be 1 because all comparisons against NaN are false.
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;
TimeSignature timeSignature = TimeSignature.SimpleQuadruple;
@@ -412,6 +416,9 @@ namespace osu.Game.Beatmaps.Formats
if (timingChange)
{
+ if (double.IsNaN(beatLength))
+ throw new InvalidDataException("Beat length cannot be NaN in a timing control point");
+
var controlPoint = CreateTimingControlPoint();
controlPoint.BeatLength = beatLength;
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 52e760a068..3d65ab8e0f 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -168,11 +168,18 @@ namespace osu.Game.Beatmaps.Formats
///
public double BpmMultiplier { get; private set; }
+ ///
+ /// Whether or not slider ticks should be generated at this control point.
+ /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991).
+ ///
+ public bool GenerateTicks { get; private set; } = true;
+
public LegacyDifficultyControlPoint(double beatLength)
: this()
{
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
+ GenerateTicks = !double.IsNaN(beatLength);
}
public LegacyDifficultyControlPoint()
@@ -180,11 +187,16 @@ namespace osu.Game.Beatmaps.Formats
SliderVelocityBindable.Precision = double.Epsilon;
}
+ public override bool IsRedundant(ControlPoint? existing)
+ => base.IsRedundant(existing)
+ && GenerateTicks == ((existing as LegacyDifficultyControlPoint)?.GenerateTicks ?? true);
+
public override void CopyFrom(ControlPoint other)
{
base.CopyFrom(other);
BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier;
+ GenerateTicks = ((LegacyDifficultyControlPoint)other).GenerateTicks;
}
public override bool Equals(ControlPoint? other)
@@ -193,10 +205,11 @@ namespace osu.Game.Beatmaps.Formats
public bool Equals(LegacyDifficultyControlPoint? other)
=> base.Equals(other)
- && BpmMultiplier == other.BpmMultiplier;
+ && BpmMultiplier == other.BpmMultiplier
+ && GenerateTicks == other.GenerateTicks;
- // ReSharper disable once NonReadonlyMemberInGetHashCode
- public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier);
+ // ReSharper disable twice NonReadonlyMemberInGetHashCode
+ public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier, GenerateTicks);
}
internal class LegacySampleControlPoint : SampleControlPoint, IEquatable
diff --git a/osu.Game/Beatmaps/Formats/Parsing.cs b/osu.Game/Beatmaps/Formats/Parsing.cs
index ce04f27020..9b0d200077 100644
--- a/osu.Game/Beatmaps/Formats/Parsing.cs
+++ b/osu.Game/Beatmaps/Formats/Parsing.cs
@@ -17,26 +17,26 @@ namespace osu.Game.Beatmaps.Formats
public const double MAX_PARSE_VALUE = int.MaxValue;
- public static float ParseFloat(string input, float parseLimit = (float)MAX_PARSE_VALUE)
+ public static float ParseFloat(string input, float parseLimit = (float)MAX_PARSE_VALUE, bool allowNaN = false)
{
float output = float.Parse(input, CultureInfo.InvariantCulture);
if (output < -parseLimit) throw new OverflowException("Value is too low");
if (output > parseLimit) throw new OverflowException("Value is too high");
- if (float.IsNaN(output)) throw new FormatException("Not a number");
+ if (!allowNaN && float.IsNaN(output)) throw new FormatException("Not a number");
return output;
}
- public static double ParseDouble(string input, double parseLimit = MAX_PARSE_VALUE)
+ public static double ParseDouble(string input, double parseLimit = MAX_PARSE_VALUE, bool allowNaN = false)
{
double output = double.Parse(input, CultureInfo.InvariantCulture);
if (output < -parseLimit) throw new OverflowException("Value is too low");
if (output > parseLimit) throw new OverflowException("Value is too high");
- if (double.IsNaN(output)) throw new FormatException("Not a number");
+ if (!allowNaN && double.IsNaN(output)) throw new FormatException("Not a number");
return output;
}
diff --git a/osu.Game/Database/TooManyDownloadsNotification.cs b/osu.Game/Database/TooManyDownloadsNotification.cs
index aa88fed43c..14012e1d34 100644
--- a/osu.Game/Database/TooManyDownloadsNotification.cs
+++ b/osu.Game/Database/TooManyDownloadsNotification.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Database
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- IconBackground.Colour = colours.RedDark;
+ IconContent.Colour = colours.RedDark;
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
index 1984840553..e5341cfd4b 100644
--- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -40,30 +42,27 @@ namespace osu.Game.Graphics.UserInterface
Margin = new MarginPadding { Left = 2 },
};
- private readonly Sample?[] textAddedSamples = new Sample[4];
- private Sample? capsTextAddedSample;
- private Sample? textRemovedSample;
- private Sample? textCommittedSample;
- private Sample? caretMovedSample;
-
- private Sample? selectCharSample;
- private Sample? selectWordSample;
- private Sample? selectAllSample;
- private Sample? deselectSample;
-
private OsuCaret? caret;
private bool selectionStarted;
private double sampleLastPlaybackTime;
- private enum SelectionSampleType
+ private enum FeedbackSampleType
{
- Character,
- Word,
- All,
+ TextAdd,
+ TextAddCaps,
+ TextRemove,
+ TextConfirm,
+ TextInvalid,
+ CaretMove,
+ SelectCharacter,
+ SelectWord,
+ SelectAll,
Deselect
}
+ private Dictionary sampleMap = new Dictionary();
+
public OsuTextBox()
{
Height = 40;
@@ -87,18 +86,23 @@ namespace osu.Game.Graphics.UserInterface
Placeholder.Colour = colourProvider?.Foreground1 ?? new Color4(180, 180, 180, 255);
+ var textAddedSamples = new Sample?[4];
for (int i = 0; i < textAddedSamples.Length; i++)
textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}");
- capsTextAddedSample = audio.Samples.Get(@"Keyboard/key-caps");
- textRemovedSample = audio.Samples.Get(@"Keyboard/key-delete");
- textCommittedSample = audio.Samples.Get(@"Keyboard/key-confirm");
- caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement");
-
- selectCharSample = audio.Samples.Get(@"Keyboard/select-char");
- selectWordSample = audio.Samples.Get(@"Keyboard/select-word");
- selectAllSample = audio.Samples.Get(@"Keyboard/select-all");
- deselectSample = audio.Samples.Get(@"Keyboard/deselect");
+ sampleMap = new Dictionary
+ {
+ { FeedbackSampleType.TextAdd, textAddedSamples },
+ { FeedbackSampleType.TextAddCaps, new[] { audio.Samples.Get(@"Keyboard/key-caps") } },
+ { FeedbackSampleType.TextRemove, new[] { audio.Samples.Get(@"Keyboard/key-delete") } },
+ { FeedbackSampleType.TextConfirm, new[] { audio.Samples.Get(@"Keyboard/key-confirm") } },
+ { FeedbackSampleType.TextInvalid, new[] { audio.Samples.Get(@"Keyboard/key-invalid") } },
+ { FeedbackSampleType.CaretMove, new[] { audio.Samples.Get(@"Keyboard/key-movement") } },
+ { FeedbackSampleType.SelectCharacter, new[] { audio.Samples.Get(@"Keyboard/select-char") } },
+ { FeedbackSampleType.SelectWord, new[] { audio.Samples.Get(@"Keyboard/select-word") } },
+ { FeedbackSampleType.SelectAll, new[] { audio.Samples.Get(@"Keyboard/select-all") } },
+ { FeedbackSampleType.Deselect, new[] { audio.Samples.Get(@"Keyboard/deselect") } }
+ };
}
private Color4 selectionColour;
@@ -109,24 +113,34 @@ namespace osu.Game.Graphics.UserInterface
{
base.OnUserTextAdded(added);
+ if (!added.Any(CanAddCharacter))
+ return;
+
if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples)
- capsTextAddedSample?.Play();
+ playSample(FeedbackSampleType.TextAddCaps);
else
- playTextAddedSample();
+ playSample(FeedbackSampleType.TextAdd);
}
protected override void OnUserTextRemoved(string removed)
{
base.OnUserTextRemoved(removed);
- textRemovedSample?.Play();
+ playSample(FeedbackSampleType.TextRemove);
+ }
+
+ protected override void NotifyInputError()
+ {
+ base.NotifyInputError();
+
+ playSample(FeedbackSampleType.TextInvalid);
}
protected override void OnTextCommitted(bool textChanged)
{
base.OnTextCommitted(textChanged);
- textCommittedSample?.Play();
+ playSample(FeedbackSampleType.TextConfirm);
}
protected override void OnCaretMoved(bool selecting)
@@ -134,7 +148,7 @@ namespace osu.Game.Graphics.UserInterface
base.OnCaretMoved(selecting);
if (!selecting)
- caretMovedSample?.Play();
+ playSample(FeedbackSampleType.CaretMove);
}
protected override void OnTextSelectionChanged(TextSelectionType selectionType)
@@ -144,15 +158,15 @@ namespace osu.Game.Graphics.UserInterface
switch (selectionType)
{
case TextSelectionType.Character:
- playSelectSample(SelectionSampleType.Character);
+ playSample(FeedbackSampleType.SelectCharacter);
break;
case TextSelectionType.Word:
- playSelectSample(selectionStarted ? SelectionSampleType.Character : SelectionSampleType.Word);
+ playSample(selectionStarted ? FeedbackSampleType.SelectCharacter : FeedbackSampleType.SelectWord);
break;
case TextSelectionType.All:
- playSelectSample(SelectionSampleType.All);
+ playSample(FeedbackSampleType.SelectAll);
break;
}
@@ -165,7 +179,7 @@ namespace osu.Game.Graphics.UserInterface
if (!selectionStarted) return;
- playSelectSample(SelectionSampleType.Deselect);
+ playSample(FeedbackSampleType.Deselect);
selectionStarted = false;
}
@@ -184,13 +198,13 @@ namespace osu.Game.Graphics.UserInterface
case 1:
// composition probably ended by pressing backspace, or was cancelled.
- textRemovedSample?.Play();
+ playSample(FeedbackSampleType.TextRemove);
return;
default:
// longer text removed, composition ended because it was cancelled.
// could be a different sample if desired.
- textRemovedSample?.Play();
+ playSample(FeedbackSampleType.TextRemove);
return;
}
}
@@ -198,7 +212,7 @@ namespace osu.Game.Graphics.UserInterface
if (addedTextLength > 0)
{
// some text was added, probably due to typing new text or by changing the candidate.
- playTextAddedSample();
+ playSample(FeedbackSampleType.TextAdd);
return;
}
@@ -206,14 +220,14 @@ namespace osu.Game.Graphics.UserInterface
{
// text was probably removed by backspacing.
// it's also possible that a candidate that only removed text was changed to.
- textRemovedSample?.Play();
+ playSample(FeedbackSampleType.TextRemove);
return;
}
if (caretMoved)
{
// only the caret/selection was moved.
- caretMovedSample?.Play();
+ playSample(FeedbackSampleType.CaretMove);
}
}
@@ -224,13 +238,13 @@ namespace osu.Game.Graphics.UserInterface
if (successful)
{
// composition was successfully completed, usually by pressing the enter key.
- textCommittedSample?.Play();
+ playSample(FeedbackSampleType.TextConfirm);
}
else
{
// composition was prematurely ended, eg. by clicking inside the textbox.
// could be a different sample if desired.
- textCommittedSample?.Play();
+ playSample(FeedbackSampleType.TextConfirm);
}
}
@@ -259,43 +273,35 @@ namespace osu.Game.Graphics.UserInterface
SelectionColour = SelectionColour,
};
- private void playSelectSample(SelectionSampleType selectionType)
+ private SampleChannel? getSampleChannel(FeedbackSampleType feedbackSampleType)
+ {
+ var samples = sampleMap[feedbackSampleType];
+
+ if (samples == null || samples.Length == 0)
+ return null;
+
+ return samples[RNG.Next(0, samples.Length)]?.GetChannel();
+ }
+
+ private void playSample(FeedbackSampleType feedbackSample)
{
if (Time.Current < sampleLastPlaybackTime + 15) return;
- SampleChannel? channel;
- double pitch = 0.98 + RNG.NextDouble(0.04);
-
- switch (selectionType)
- {
- case SelectionSampleType.All:
- channel = selectAllSample?.GetChannel();
- break;
-
- case SelectionSampleType.Word:
- channel = selectWordSample?.GetChannel();
- break;
-
- case SelectionSampleType.Deselect:
- channel = deselectSample?.GetChannel();
- break;
-
- default:
- channel = selectCharSample?.GetChannel();
- pitch += (SelectedText.Length / (double)Text.Length) * 0.15f;
- break;
- }
+ SampleChannel? channel = getSampleChannel(feedbackSample);
if (channel == null) return;
+ double pitch = 0.98 + RNG.NextDouble(0.04);
+
+ if (feedbackSample == FeedbackSampleType.SelectCharacter)
+ pitch += ((double)SelectedText.Length / Math.Max(1, Text.Length)) * 0.15f;
+
channel.Frequency.Value = pitch;
channel.Play();
sampleLastPlaybackTime = Time.Current;
}
- private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play();
-
private class OsuCaret : Caret
{
private const float caret_move_time = 60;
diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs
index 3fc6a008e8..22c2b4690e 100644
--- a/osu.Game/Online/Chat/MessageNotifier.cs
+++ b/osu.Game/Online/Chat/MessageNotifier.cs
@@ -174,7 +174,7 @@ namespace osu.Game.Online.Chat
[BackgroundDependencyLoader]
private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay)
{
- IconBackground.Colour = colours.PurpleDark;
+ IconContent.Colour = colours.PurpleDark;
Activated = delegate
{
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index f7747c5d64..108153fd9d 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -804,8 +804,8 @@ namespace osu.Game
Children = new Drawable[]
{
overlayContent = new Container { RelativeSizeAxes = Axes.Both },
- rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
+ rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
}
},
topMostOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs
index cbcc7b6886..2c39ebcc87 100644
--- a/osu.Game/Overlays/NotificationOverlay.cs
+++ b/osu.Game/Overlays/NotificationOverlay.cs
@@ -12,10 +12,10 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Threading;
-using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Notifications;
using osu.Game.Resources.Localisation.Web;
+using osuTK;
using NotificationsStrings = osu.Game.Localisation.NotificationsStrings;
namespace osu.Game.Overlays
@@ -35,10 +35,28 @@ namespace osu.Game.Overlays
[Resolved]
private AudioManager audio { get; set; } = null!;
- private readonly IBindable firstRunSetupVisibility = new Bindable();
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
+ {
+ if (State.Value == Visibility.Visible)
+ return base.ReceivePositionalInputAt(screenSpacePos);
+
+ if (toastTray.IsDisplayingToasts)
+ return toastTray.ReceivePositionalInputAt(screenSpacePos);
+
+ return false;
+ }
+
+ public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree || toastTray.IsDisplayingToasts;
+
+ private NotificationOverlayToastTray toastTray = null!;
+
+ private Container mainContent = null!;
[BackgroundDependencyLoader]
- private void load(FirstRunSetupOverlay? firstRunSetup)
+ private void load()
{
X = WIDTH;
Width = WIDTH;
@@ -46,47 +64,57 @@ namespace osu.Game.Overlays
Children = new Drawable[]
{
- new Box
+ toastTray = new NotificationOverlayToastTray
{
- RelativeSizeAxes = Axes.Both,
- Colour = OsuColour.Gray(0.05f),
+ ForwardNotificationToPermanentStore = addPermanently,
+ Origin = Anchor.TopRight,
},
- new OsuScrollContainer
+ mainContent = new Container
{
- Masking = true,
+ AlwaysPresent = true,
RelativeSizeAxes = Axes.Both,
- Children = new[]
+ Children = new Drawable[]
{
- sections = new FillFlowContainer
+ new Box
{
- Direction = FillDirection.Vertical,
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4,
+ },
+ new OsuScrollContainer
+ {
+ Masking = true,
+ RelativeSizeAxes = Axes.Both,
Children = new[]
{
- new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, "Clear All"),
- new NotificationSection(@"Running Tasks", new[] { typeof(ProgressNotification) }, @"Cancel All"),
+ sections = new FillFlowContainer
+ {
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Children = new[]
+ {
+ new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, "Clear All"),
+ new NotificationSection(@"Running Tasks", new[] { typeof(ProgressNotification) }, @"Cancel All"),
+ }
+ }
}
}
}
- }
+ },
};
-
- if (firstRunSetup != null)
- firstRunSetupVisibility.BindTo(firstRunSetup.State);
}
private ScheduledDelegate? notificationsEnabler;
private void updateProcessingMode()
{
- bool enabled = (OverlayActivationMode.Value == OverlayActivation.All && firstRunSetupVisibility.Value != Visibility.Visible) || State.Value == Visibility.Visible;
+ bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible;
notificationsEnabler?.Cancel();
if (enabled)
// we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed.
- notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 1000);
+ notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 100);
else
processingPosts = false;
}
@@ -96,12 +124,13 @@ namespace osu.Game.Overlays
base.LoadComplete();
State.BindValueChanged(_ => updateProcessingMode());
- firstRunSetupVisibility.BindValueChanged(_ => updateProcessingMode());
OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true);
}
public IBindable UnreadCount => unreadCount;
+ public int ToastCount => toastTray.UnreadCount;
+
private readonly BindableInt unreadCount = new BindableInt();
private int runningDepth;
@@ -125,18 +154,28 @@ namespace osu.Game.Overlays
if (notification is IHasCompletionTarget hasCompletionTarget)
hasCompletionTarget.CompletionTarget = Post;
- var ourType = notification.GetType();
+ playDebouncedSample(notification.PopInSampleName);
- var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType)));
- section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth);
-
- if (notification.IsImportant)
- Show();
+ if (State.Value == Visibility.Hidden)
+ toastTray.Post(notification);
+ else
+ addPermanently(notification);
updateCounts();
- playDebouncedSample(notification.PopInSampleName);
});
+ private void addPermanently(Notification notification)
+ {
+ var ourType = notification.GetType();
+ int depth = notification.DisplayOnTop ? -runningDepth : runningDepth;
+
+ var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType)));
+
+ section.Add(notification, depth);
+
+ updateCounts();
+ }
+
protected override void Update()
{
base.Update();
@@ -150,7 +189,9 @@ namespace osu.Game.Overlays
base.PopIn();
this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint);
- this.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint);
+ mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint);
+
+ toastTray.FlushAllToasts();
}
protected override void PopOut()
@@ -160,7 +201,7 @@ namespace osu.Game.Overlays
markAllRead();
this.MoveToX(WIDTH, TRANSITION_LENGTH, Easing.OutQuint);
- this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint);
+ mainContent.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint);
}
private void notificationClosed()
@@ -181,16 +222,16 @@ namespace osu.Game.Overlays
}
}
- private void updateCounts()
- {
- unreadCount.Value = sections.Select(c => c.UnreadCount).Sum();
- }
-
private void markAllRead()
{
sections.Children.ForEach(s => s.MarkAllRead());
-
+ toastTray.MarkAllRead();
updateCounts();
}
+
+ private void updateCounts()
+ {
+ unreadCount.Value = sections.Select(c => c.UnreadCount).Sum() + toastTray.UnreadCount;
+ }
}
}
diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs
new file mode 100644
index 0000000000..4417b5e0d0
--- /dev/null
+++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs
@@ -0,0 +1,153 @@
+// 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.Diagnostics;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Overlays.Notifications;
+using osuTK;
+
+namespace osu.Game.Overlays
+{
+ ///
+ /// A tray which attaches to the left of to show temporary toasts.
+ ///
+ public class NotificationOverlayToastTray : CompositeDrawable
+ {
+ public bool IsDisplayingToasts => toastFlow.Count > 0;
+
+ private FillFlowContainer toastFlow = null!;
+ private BufferedContainer toastContentBackground = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ public Action? ForwardNotificationToPermanentStore { get; set; }
+
+ public int UnreadCount => toastFlow.Count(n => !n.WasClosed && !n.Read)
+ + InternalChildren.OfType().Count(n => !n.WasClosed && !n.Read);
+
+ private int runningDepth;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ Padding = new MarginPadding(20);
+
+ InternalChildren = new Drawable[]
+ {
+ toastContentBackground = (new Box
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Colour = ColourInfo.GradientVertical(
+ colourProvider.Background6.Opacity(0.7f),
+ colourProvider.Background6.Opacity(0.5f)),
+ RelativeSizeAxes = Axes.Both,
+ }.WithEffect(new BlurEffect
+ {
+ PadExtent = true,
+ Sigma = new Vector2(20),
+ }).With(postEffectDrawable =>
+ {
+ postEffectDrawable.Scale = new Vector2(1.5f, 1);
+ postEffectDrawable.Position += new Vector2(70, -50);
+ postEffectDrawable.AutoSizeAxes = Axes.None;
+ postEffectDrawable.RelativeSizeAxes = Axes.X;
+ })),
+ toastFlow = new AlwaysUpdateFillFlowContainer
+ {
+ LayoutDuration = 150,
+ LayoutEasing = Easing.OutQuart,
+ Spacing = new Vector2(3),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ };
+ }
+
+ public void MarkAllRead()
+ {
+ toastFlow.Children.ForEach(n => n.Read = true);
+ InternalChildren.OfType().ForEach(n => n.Read = true);
+ }
+
+ public void FlushAllToasts()
+ {
+ foreach (var notification in toastFlow.ToArray())
+ forwardNotification(notification);
+ }
+
+ public void Post(Notification notification)
+ {
+ ++runningDepth;
+
+ int depth = notification.DisplayOnTop ? -runningDepth : runningDepth;
+
+ toastFlow.Insert(depth, notification);
+
+ scheduleDismissal();
+
+ void scheduleDismissal() => Scheduler.AddDelayed(() =>
+ {
+ // Notification dismissed by user.
+ if (notification.WasClosed)
+ return;
+
+ // Notification forwarded away.
+ if (notification.Parent != toastFlow)
+ return;
+
+ // Notification hovered; delay dismissal.
+ if (notification.IsHovered)
+ {
+ scheduleDismissal();
+ return;
+ }
+
+ // All looks good, forward away!
+ forwardNotification(notification);
+ }, notification.IsImportant ? 12000 : 2500);
+ }
+
+ private void forwardNotification(Notification notification)
+ {
+ Debug.Assert(notification.Parent == toastFlow);
+
+ // Temporarily remove from flow so we can animate the position off to the right.
+ toastFlow.Remove(notification);
+ AddInternal(notification);
+
+ notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint);
+ notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ =>
+ {
+ RemoveInternal(notification);
+ ForwardNotificationToPermanentStore?.Invoke(notification);
+
+ notification.FadeIn(300, Easing.OutQuint);
+ });
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ float height = toastFlow.DrawHeight + 120;
+ float alpha = IsDisplayingToasts ? MathHelper.Clamp(toastFlow.DrawHeight / 40, 0, 1) * toastFlow.Children.Max(n => n.Alpha) : 0;
+
+ toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime);
+ toastContentBackground.Alpha = (float)Interpolation.DampContinuously(toastContentBackground.Alpha, alpha, 10, Clock.ElapsedFrameTime);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs
index fbb906e637..e5f739bb08 100644
--- a/osu.Game/Overlays/Notifications/Notification.cs
+++ b/osu.Game/Overlays/Notifications/Notification.cs
@@ -46,22 +46,32 @@ namespace osu.Game.Overlays.Notifications
public virtual string PopInSampleName => "UI/notification-pop-in";
protected NotificationLight Light;
- private readonly CloseButton closeButton;
+
protected Container IconContent;
+
private readonly Container content;
protected override Container Content => content;
- protected Container NotificationContent;
+ protected Container MainContent;
public virtual bool Read { get; set; }
+ protected virtual IconUsage CloseButtonIcon => FontAwesome.Solid.Check;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ private readonly Box initialFlash;
+
+ private Box background = null!;
+
protected Notification()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- AddRangeInternal(new Drawable[]
+ InternalChildren = new Drawable[]
{
Light = new NotificationLight
{
@@ -69,9 +79,9 @@ namespace osu.Game.Overlays.Notifications
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
},
- NotificationContent = new Container
+ MainContent = new Container
{
- CornerRadius = 8,
+ CornerRadius = 6,
Masking = true,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -79,61 +89,84 @@ namespace osu.Game.Overlays.Notifications
AutoSizeEasing = Easing.OutQuint,
Children = new Drawable[]
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.White,
- },
- new Container
+ new GridContainer
{
RelativeSizeAxes = Axes.X,
- Padding = new MarginPadding(5),
AutoSizeAxes = Axes.Y,
- Children = new Drawable[]
+ RowDimensions = new[]
{
- IconContent = new Container
- {
- Size = new Vector2(40),
- Masking = true,
- CornerRadius = 5,
- },
- content = new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding
- {
- Left = 45,
- Right = 30
- },
- }
- }
- },
- closeButton = new CloseButton
- {
- Alpha = 0,
- Action = Close,
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- Margin = new MarginPadding
- {
- Right = 5
+ new Dimension(GridSizeMode.AutoSize, minSize: 60)
},
- }
+ ColumnDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ IconContent = new Container
+ {
+ Width = 40,
+ RelativeSizeAxes = Axes.Y,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding(10),
+ Children = new Drawable[]
+ {
+ content = new Container
+ {
+ Masking = true,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ }
+ },
+ new CloseButton(CloseButtonIcon)
+ {
+ Action = Close,
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ }
+ }
+ },
+ },
+ initialFlash = new Box
+ {
+ Colour = Color4.White.Opacity(0.8f),
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ },
}
}
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ MainContent.Add(background = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background3,
+ Depth = float.MaxValue
});
}
protected override bool OnHover(HoverEvent e)
{
- closeButton.FadeIn(75);
+ background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
- closeButton.FadeOut(75);
+ background.FadeColour(colourProvider.Background3, 200, Easing.OutQuint);
base.OnHoverLost(e);
}
@@ -150,8 +183,11 @@ namespace osu.Game.Overlays.Notifications
base.LoadComplete();
this.FadeInFromZero(200);
- NotificationContent.MoveToX(DrawSize.X);
- NotificationContent.MoveToX(0, 500, Easing.OutQuint);
+
+ MainContent.MoveToX(DrawSize.X);
+ MainContent.MoveToX(0, 500, Easing.OutQuint);
+
+ initialFlash.FadeOutFromOne(2000, Easing.OutQuart);
}
public bool WasClosed;
@@ -169,40 +205,55 @@ namespace osu.Game.Overlays.Notifications
private class CloseButton : OsuClickableContainer
{
- private Color4 hoverColour;
+ private SpriteIcon icon = null!;
+ private Box background = null!;
- public CloseButton()
+ private readonly IconUsage iconUsage;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ public CloseButton(IconUsage iconUsage)
{
- Colour = OsuColour.Gray(0.2f);
- AutoSizeAxes = Axes.Both;
+ this.iconUsage = iconUsage;
+ }
- Children = new[]
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.Y;
+ Width = 28;
+
+ Children = new Drawable[]
{
- new SpriteIcon
+ background = new Box
+ {
+ Colour = OsuColour.Gray(0).Opacity(0.15f),
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both,
+ },
+ icon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Icon = FontAwesome.Solid.TimesCircle,
- Size = new Vector2(20),
+ Icon = iconUsage,
+ Size = new Vector2(12),
+ Colour = colourProvider.Foreground1,
}
};
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- hoverColour = colours.Yellow;
- }
-
protected override bool OnHover(HoverEvent e)
{
- this.FadeColour(hoverColour, 200);
+ background.FadeIn(200, Easing.OutQuint);
+ icon.FadeColour(colourProvider.Content1, 200, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
- this.FadeColour(OsuColour.Gray(0.2f), 200);
+ background.FadeOut(200, Easing.OutQuint);
+ icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint);
base.OnHoverLost(e);
}
}
diff --git a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs
index cb9d54c14c..49d558285c 100644
--- a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Notifications
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- IconBackground.Colour = ColourInfo.GradientVertical(colours.GreenDark, colours.GreenLight);
+ IconContent.Colour = ColourInfo.GradientVertical(colours.GreenDark, colours.GreenLight);
}
}
}
diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
index 15346930a3..14cf6b3013 100644
--- a/osu.Game/Overlays/Notifications/ProgressNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -57,11 +57,14 @@ namespace osu.Game.Overlays.Notifications
set
{
progress = value;
- Scheduler.AddOnce(updateProgress, progress);
+ Scheduler.AddOnce(p => progressBar.Progress = p, progress);
}
}
- private void updateProgress(float progress) => progressBar.Progress = progress;
+ protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
protected override void LoadComplete()
{
@@ -100,7 +103,7 @@ namespace osu.Game.Overlays.Notifications
Light.Pulsate = false;
progressBar.Active = false;
- iconBackground.FadeColour(ColourInfo.GradientVertical(colourQueued, colourQueued.Lighten(0.5f)), colour_fade_duration);
+ IconContent.FadeColour(ColourInfo.GradientVertical(colourQueued, colourQueued.Lighten(0.5f)), colour_fade_duration);
loadingSpinner.Show();
break;
@@ -109,14 +112,14 @@ namespace osu.Game.Overlays.Notifications
Light.Pulsate = true;
progressBar.Active = true;
- iconBackground.FadeColour(ColourInfo.GradientVertical(colourActive, colourActive.Lighten(0.5f)), colour_fade_duration);
+ IconContent.FadeColour(ColourInfo.GradientVertical(colourActive, colourActive.Lighten(0.5f)), colour_fade_duration);
loadingSpinner.Show();
break;
case ProgressNotificationState.Cancelled:
cancellationTokenSource.Cancel();
- iconBackground.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration);
+ IconContent.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration);
loadingSpinner.Hide();
var icon = new SpriteIcon
@@ -138,8 +141,7 @@ namespace osu.Game.Overlays.Notifications
case ProgressNotificationState.Completed:
loadingSpinner.Hide();
- NotificationContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint);
- this.FadeOut(200).Finally(_ => Completed());
+ Completed();
break;
}
}
@@ -165,21 +167,19 @@ namespace osu.Game.Overlays.Notifications
private Color4 colourActive;
private Color4 colourCancelled;
- private Box iconBackground = null!;
private LoadingSpinner loadingSpinner = null!;
private readonly TextFlowContainer textDrawable;
public ProgressNotification()
{
- Content.Add(textDrawable = new OsuTextFlowContainer
+ Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium))
{
- Colour = OsuColour.Gray(128),
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
});
- NotificationContent.Add(progressBar = new ProgressBar
+ MainContent.Add(progressBar = new ProgressBar
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
@@ -204,10 +204,10 @@ namespace osu.Game.Overlays.Notifications
IconContent.AddRange(new Drawable[]
{
- iconBackground = new Box
+ new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.White,
+ Colour = colourProvider.Background5,
},
loadingSpinner = new LoadingSpinner
{
diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs
index b9a1cc6d90..1dba60fb5f 100644
--- a/osu.Game/Overlays/Notifications/SimpleNotification.cs
+++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs
@@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
@@ -24,7 +23,8 @@ namespace osu.Game.Overlays.Notifications
set
{
text = value;
- textDrawable.Text = text;
+ if (textDrawable != null)
+ textDrawable.Text = text;
}
}
@@ -36,48 +36,44 @@ namespace osu.Game.Overlays.Notifications
set
{
icon = value;
- iconDrawable.Icon = icon;
+ if (iconDrawable != null)
+ iconDrawable.Icon = icon;
}
}
- private readonly TextFlowContainer textDrawable;
- private readonly SpriteIcon iconDrawable;
+ private TextFlowContainer? textDrawable;
- protected Box IconBackground;
+ private SpriteIcon? iconDrawable;
- public SimpleNotification()
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, OverlayColourProvider colourProvider)
{
+ Light.Colour = colours.Green;
+
IconContent.AddRange(new Drawable[]
{
- IconBackground = new Box
+ new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.6f))
+ Colour = colourProvider.Background5,
},
iconDrawable = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = icon,
- Size = new Vector2(20),
+ Size = new Vector2(16),
}
});
- Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14))
+ Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium))
{
- Colour = OsuColour.Gray(128),
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Text = text
});
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- Light.Colour = colours.Green;
- }
-
public override bool Read
{
get => base.Read;
diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs
index 06af232111..e3934025e2 100644
--- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (result != 0) return result;
}
- return CompareReverseChildID(y, x);
+ return CompareReverseChildID(x, y);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
index 1ae393d06a..6de88d7ad0 100644
--- a/osu.Game/Screens/Play/GameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -88,9 +88,7 @@ namespace osu.Game.Screens.Play
ensureSourceClockSet();
- // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
- // This accounts for the clock source potentially taking time to enter a completely stopped state
- Seek(GameplayClock.CurrentTime);
+ PrepareStart();
// The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time.
// Because we generally update our own current time quicker than children can query it (via Start/Seek/Update),
@@ -111,11 +109,19 @@ namespace osu.Game.Screens.Play
});
}
+ ///
+ /// When is called, this will be run to give an opportunity to prepare the clock at the correct
+ /// start location.
+ ///
+ protected virtual void PrepareStart()
+ {
+ }
+
///
/// Seek to a specific time in gameplay.
///
/// The destination time to seek to.
- public void Seek(double time)
+ public virtual void Seek(double time)
{
Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}");
diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
index 238817ad05..2f1ffa126f 100644
--- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
@@ -45,6 +45,17 @@ namespace osu.Game.Screens.Play
private readonly List> nonGameplayAdjustments = new List>();
+ ///
+ /// Stores the time at which the last call was triggered.
+ /// This is used to ensure we resume from that precise point in time, ignoring the proceeding frequency ramp.
+ ///
+ /// Optimally, we'd have gameplay ramp down with the frequency, but I believe this was intentionally disabled
+ /// to avoid fails occurring after the pause screen has been shown.
+ ///
+ /// In the future I want to change this.
+ ///
+ private double? actualStopTime;
+
public override IEnumerable NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value);
///
@@ -86,6 +97,8 @@ namespace osu.Game.Screens.Play
protected override void StopGameplayClock()
{
+ actualStopTime = GameplayClock.CurrentTime;
+
if (IsLoaded)
{
// During normal operation, the source is stopped after performing a frequency ramp.
@@ -108,6 +121,25 @@ namespace osu.Game.Screens.Play
}
}
+ public override void Seek(double time)
+ {
+ // Safety in case the clock is seeked while stopped.
+ actualStopTime = null;
+
+ base.Seek(time);
+ }
+
+ protected override void PrepareStart()
+ {
+ if (actualStopTime != null)
+ {
+ Seek(actualStopTime.Value);
+ actualStopTime = null;
+ }
+ else
+ base.PrepareStart();
+ }
+
protected override void StartGameplayClock()
{
addSourceClockAdjustments();
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index e6bd1367ef..a9fcab063c 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -530,7 +530,7 @@ namespace osu.Game.Screens.Play
private void load(OsuColour colours, AudioManager audioManager, INotificationOverlay notificationOverlay, VolumeOverlay volumeOverlay)
{
Icon = FontAwesome.Solid.VolumeMute;
- IconBackground.Colour = colours.RedDark;
+ IconContent.Colour = colours.RedDark;
Activated = delegate
{
@@ -584,7 +584,7 @@ namespace osu.Game.Screens.Play
private void load(OsuColour colours, INotificationOverlay notificationOverlay)
{
Icon = FontAwesome.Solid.BatteryQuarter;
- IconBackground.Colour = colours.RedDark;
+ IconContent.Colour = colours.RedDark;
Activated = delegate
{
diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs
index 5c9a706549..974d40b538 100644
--- a/osu.Game/Screens/Play/SkipOverlay.cs
+++ b/osu.Game/Screens/Play/SkipOverlay.cs
@@ -114,16 +114,17 @@ namespace osu.Game.Screens.Play
{
base.LoadComplete();
+ displayTime = gameplayClock.CurrentTime;
+
// skip is not required if there is no extra "empty" time to skip.
// we may need to remove this if rewinding before the initial player load position becomes a thing.
- if (fadeOutBeginTime < gameplayClock.CurrentTime)
+ if (fadeOutBeginTime <= displayTime)
{
Expire();
return;
}
button.Action = () => RequestSkip?.Invoke();
- displayTime = gameplayClock.CurrentTime;
fadeContainer.TriggerShow();
@@ -146,7 +147,12 @@ namespace osu.Game.Screens.Play
{
base.Update();
- double progress = fadeOutBeginTime <= displayTime ? 1 : Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime));
+ // This case causes an immediate expire in `LoadComplete`, but `Update` may run once after that.
+ // Avoid div-by-zero below.
+ if (fadeOutBeginTime <= displayTime)
+ return;
+
+ double progress = Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime));
remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1));
diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs
index d60f2e4a4b..4790055cd1 100644
--- a/osu.Game/Updater/UpdateManager.cs
+++ b/osu.Game/Updater/UpdateManager.cs
@@ -99,7 +99,7 @@ namespace osu.Game.Updater
private void load(OsuColour colours, ChangelogOverlay changelog, INotificationOverlay notificationOverlay)
{
Icon = FontAwesome.Solid.CheckSquare;
- IconBackground.Colour = colours.BlueDark;
+ IconContent.Colour = colours.BlueDark;
Activated = delegate
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 5fc69e475f..f757fd77b9 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -37,7 +37,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index f763e411be..9fcc3753eb 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -62,7 +62,7 @@
-
+