diff --git a/osu.Android.props b/osu.Android.props
index 7060e88026..e30416bc1c 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 788e5f82be..ad929bbac3 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -20,7 +20,7 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
- [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream" })]
+ [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index d1515acafa..5909b82c8f 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -18,6 +18,7 @@ using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
using osu.Desktop.Windows;
+using osu.Game.IO;
namespace osu.Desktop
{
@@ -32,7 +33,7 @@ namespace osu.Desktop
noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false;
}
- public override Storage GetStorageForStableInstall()
+ public override StableStorage GetStorageForStableInstall()
{
try
{
@@ -40,7 +41,7 @@ namespace osu.Desktop
{
string stablePath = getStableInstallPath();
if (!string.IsNullOrEmpty(stablePath))
- return new DesktopStorage(stablePath, desktopHost);
+ return new StableStorage(stablePath, desktopHost);
}
}
catch (Exception)
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 7805bfcefc..ea43d9a54c 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 54fddc297e..bf3aba5859 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
index 692e63fa69..e1eceea606 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
index 3bc1ee5bf5..d53d019e90 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModCinema : ModCinema
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index 438d17dbc5..5f1736450a 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
- public BindableNumber CircleSize { get; } = new BindableFloat
+ public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 1,
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
- public BindableNumber ApproachRate { get; } = new BindableFloat
+ public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 1,
@@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
+ protected override void ApplyLimits(bool extended)
+ {
+ base.ApplyLimits(extended);
+
+ CircleSize.MaxValue = extended ? 11 : 10;
+ ApproachRate.MaxValue = extended ? 11 : 10;
+ }
+
public override string SettingDescription
{
get
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index d55b4fe08a..fcc0cafefc 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
index c05e979e9a..105d88129c 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
index 02c1fc1b79..064c55ed8d 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModCinema : ModCinema
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index 7e2a8823b6..cbbbacfe19 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -140,11 +140,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return animation == null ? null : new LegacyManiaJudgementPiece(result, animation);
}
- public override SampleChannel GetSample(ISampleInfo sampleInfo)
+ public override Sample GetSample(ISampleInfo sampleInfo)
{
// layered hit sounds never play in mania
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
- return new SampleChannelVirtual();
+ return new SampleVirtual();
return Source.GetSample(sampleInfo);
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
new file mode 100644
index 0000000000..856b6554b9
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Rulesets.Osu.UI;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModAutoplay : OsuModTestScene
+ {
+ [Test]
+ public void TestSpmUnaffectedByRateAdjust()
+ => runSpmTest(new OsuModDaycore
+ {
+ SpeedChange = { Value = 0.88 }
+ });
+
+ [Test]
+ public void TestSpmUnaffectedByTimeRamp()
+ => runSpmTest(new ModWindUp
+ {
+ InitialRate = { Value = 0.7 },
+ FinalRate = { Value = 1.3 }
+ });
+
+ private void runSpmTest(Mod mod)
+ {
+ SpinnerSpmCounter spmCounter = null;
+
+ CreateModTest(new ModTestData
+ {
+ Autoplay = true,
+ Mod = mod,
+ Beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new Spinner
+ {
+ Duration = 2000,
+ Position = OsuPlayfield.BASE_SIZE / 2
+ }
+ }
+ },
+ PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
+ });
+
+ AddUntilStep("fetch SPM counter", () =>
+ {
+ spmCounter = this.ChildrenOfType().SingleOrDefault();
+ return spmCounter != null;
+ });
+
+ AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
index fefe983f97..e2d9f144c0 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Tests
return null;
}
- public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
index 39deba2f57..af67ab5839 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs
@@ -1,9 +1,11 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
@@ -65,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestAutoMod : OsuModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
- Replay = new MissingAutoGenerator(beatmap).Generate()
+ Replay = new MissingAutoGenerator(beatmap, mods).Generate()
};
}
@@ -76,8 +78,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap;
- public MissingAutoGenerator(IBeatmap beatmap)
- : base(beatmap)
+ public MissingAutoGenerator(IBeatmap beatmap, IReadOnlyList mods)
+ : base(beatmap, mods)
{
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
new file mode 100644
index 0000000000..77a68b714b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
@@ -0,0 +1,491 @@
+// 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 NUnit.Framework;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Screens;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
+ {
+ private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss
+ private const double late_miss_window = 500; // time after +500 is considered a miss
+
+ ///
+ /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleBeforeFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Miss);
+ addJudgementOffsetAssert(hitObjects[0], late_miss_window);
+ }
+
+ ///
+ /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleAtFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Miss);
+ addJudgementOffsetAssert(hitObjects[0], late_miss_window);
+ }
+
+ ///
+ /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleAfterFirstCircleTime()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Miss);
+ addJudgementOffsetAssert(hitObjects[0], late_miss_window);
+ }
+
+ ///
+ /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
+ addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100
+ }
+
+ ///
+ /// Tests clicking a future circle after the first circle's start time, while the first circle HAS been judged.
+ ///
+ [Test]
+ public void TestClickSecondCircleAfterFirstCircleTimeWithFirstCircleJudged()
+ {
+ const double time_first_circle = 1500;
+ const double time_second_circle = 1600;
+ Vector2 positionFirstCircle = Vector2.Zero;
+ Vector2 positionSecondCircle = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_first_circle,
+ Position = positionFirstCircle
+ },
+ new TestHitCircle
+ {
+ StartTime = time_second_circle,
+ Position = positionSecondCircle
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
+ addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
+ }
+
+ ///
+ /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
+ ///
+ [Test]
+ public void TestMissSliderHeadAndHitAllSliderTicks()
+ {
+ const double time_slider = 1500;
+ const double time_circle = 1510;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Miss);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
+ }
+
+ ///
+ /// Tests clicking hitting future slider ticks before a circle.
+ ///
+ [Test]
+ public void TestHitSliderTicksBeforeCircle()
+ {
+ const double time_slider = 1500;
+ const double time_circle = 1510;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(30);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
+ addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
+ }
+
+ ///
+ /// Tests clicking a future circle before a spinner.
+ ///
+ [Test]
+ public void TestHitCircleBeforeSpinner()
+ {
+ const double time_spinner = 1500;
+ const double time_circle = 1800;
+ Vector2 positionCircle = Vector2.Zero;
+
+ var hitObjects = new List
+ {
+ new TestSpinner
+ {
+ StartTime = time_spinner,
+ Position = new Vector2(256, 192),
+ EndTime = time_spinner + 1000,
+ },
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ }
+
+ [Test]
+ public void TestHitSliderHeadBeforeHitCircle()
+ {
+ const double time_circle = 1000;
+ const double time_slider = 1200;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ }
+
+ private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
+ }
+
+ private void addJudgementAssert(string name, Func hitObject, HitResult result)
+ {
+ AddAssert($"{name} judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result);
+ }
+
+ private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
+ {
+ AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
+ () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
+ }
+
+ private ScoreAccessibleReplayPlayer currentPlayer;
+ private List judgementResults;
+
+ private void performTest(List hitObjects, List frames)
+ {
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ HitObjects = hitObjects,
+ BeatmapInfo =
+ {
+ BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
+ Ruleset = new OsuRuleset().RulesetInfo
+ },
+ });
+
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+
+ SelectedMods.Value = new[] { new OsuModClassic() };
+
+ var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
+
+ p.OnLoadComplete += _ =>
+ {
+ p.ScoreProcessor.NewJudgement += result =>
+ {
+ if (currentPlayer == p) judgementResults.Add(result);
+ };
+ };
+
+ LoadScreen(currentPlayer = p);
+ judgementResults = new List();
+ });
+
+ AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
+ AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+ AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
+ }
+
+ private class TestHitCircle : HitCircle
+ {
+ protected override HitWindows CreateHitWindows() => new TestHitWindows();
+ }
+
+ private class TestSlider : Slider
+ {
+ public TestSlider()
+ {
+ DefaultsApplied += _ =>
+ {
+ HeadCircle.HitWindows = new TestHitWindows();
+ TailCircle.HitWindows = new TestHitWindows();
+
+ HeadCircle.HitWindows.SetDifficulty(0);
+ TailCircle.HitWindows.SetDifficulty(0);
+ };
+ }
+ }
+
+ private class TestSpinner : Spinner
+ {
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+ SpinsRequired = 1;
+ }
+ }
+
+ private class TestHitWindows : HitWindows
+ {
+ private static readonly DifficultyRange[] ranges =
+ {
+ new DifficultyRange(HitResult.Great, 500, 500, 500),
+ new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
+ };
+
+ public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
+
+ protected override DifficultyRange[] GetRanges() => ranges;
+ }
+
+ private class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ protected override bool PauseOnFocusLost => false;
+
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score, new PlayerConfiguration
+ {
+ AllowPause = false,
+ ShowResults = false,
+ })
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index 10baca438d..8dbb48c048 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
- public SampleChannel GetSample(ISampleInfo sampleInfo) => null;
+ public Sample GetSample(ISampleInfo sampleInfo) => null;
public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => default;
public IBindable GetConfig(TLookup lookup) => null;
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 345c3e6d35..b4c686ccea 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
index a0392fe536..dec9cd8622 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
base.Update();
- CirclePiece.UpdateFrom(position == SliderPosition.Start ? HitObject.HeadCircle : HitObject.TailCircle);
+ CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)HitObject.HeadCircle : HitObject.TailCircle);
}
// Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input.
diff --git a/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs
new file mode 100644
index 0000000000..a088696784
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Judgements
+{
+ public class SliderTickJudgement : OsuJudgement
+ {
+ public override HitResult MaxResult => HitResult.LargeTickHit;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 8c819c4773..77de0cb45b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
inputManager.AllowUserCursorMovement = false;
// Generate the replay frames the cursor should follow
- replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap).Generate().Frames.Cast().ToList();
+ replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast().ToList();
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
index bea2bbcb32..3b1f271d41 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -16,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
- Replay = new OsuAutoGenerator(beatmap).Generate()
+ Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
index 5d9a524577..df06988b70 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -16,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
- Replay = new OsuAutoGenerator(beatmap).Generate()
+ Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
new file mode 100644
index 0000000000..5470d0fcb4
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
@@ -0,0 +1,86 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset
+ {
+ public override string Name => "Classic";
+
+ public override string Acronym => "CL";
+
+ public override double ScoreMultiplier => 1;
+
+ public override IconUsage? Icon => FontAwesome.Solid.History;
+
+ public override string Description => "Feeling nostalgic?";
+
+ public override bool Ranked => false;
+
+ public override ModType Type => ModType.Conversion;
+
+ [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
+ public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true);
+
+ [SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")]
+ public Bindable NoSliderHeadMovement { get; } = new BindableBool(true);
+
+ [SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
+ public Bindable ClassicNoteLock { get; } = new BindableBool(true);
+
+ [SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")]
+ public Bindable FixedFollowCircleHitArea { get; } = new BindableBool(true);
+
+ public void ApplyToHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case Slider slider:
+ slider.OnlyJudgeNestedObjects = !NoSliderHeadAccuracy.Value;
+
+ foreach (var head in slider.NestedHitObjects.OfType())
+ head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value;
+
+ break;
+ }
+ }
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
+
+ if (ClassicNoteLock.Value)
+ osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
+ }
+
+ public void ApplyToDrawableHitObjects(IEnumerable drawables)
+ {
+ foreach (var obj in drawables)
+ {
+ switch (obj)
+ {
+ case DrawableSlider slider:
+ slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
+ break;
+
+ case DrawableSliderHead head:
+ head.TrackFollowCircle = !NoSliderHeadMovement.Value;
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
index a638234dbd..1cb25edecf 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
- public BindableNumber CircleSize { get; } = new BindableFloat
+ public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
- public BindableNumber ApproachRate { get; } = new BindableFloat
+ public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Osu.Mods
Value = 5,
};
+ protected override void ApplyLimits(bool extended)
+ {
+ base.ApplyLimits(extended);
+
+ CircleSize.MaxValue = extended ? 11 : 10;
+ ApproachRate.MaxValue = extended ? 11 : 10;
+ }
+
public override string SettingDescription
{
get
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
index 6e7b1050cb..5541d0e790 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -110,8 +110,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
double startTime = start.GetEndTime();
double duration = end.StartTime - startTime;
+ // Preempt time can go below 800ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
+ // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear preempt function (see: OsuHitObject).
+ // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good.
+ double preempt = PREEMPT * Math.Min(1, start.TimePreempt / OsuHitObject.PREEMPT_MIN);
+
fadeOutTime = startTime + fraction * duration;
- fadeInTime = fadeOutTime - PREEMPT;
+ fadeInTime = fadeOutTime - preempt;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 3c0260f5f5..77094f928b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return;
}
- var result = HitObject.HitWindows.ResultFor(timeOffset);
+ var result = ResultFor(timeOffset);
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
@@ -146,6 +146,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
+ ///
+ /// Retrieves the for a time offset.
+ ///
+ /// The time offset.
+ /// The hit result, or if doesn't result in a judgement.
+ protected virtual HitResult ResultFor(double timeOffset) => HitObject.HitWindows.ResultFor(timeOffset);
+
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 13f5960bd4..79655c33e4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (JudgedObject?.HitObject is OsuHitObject osuObject)
{
- Position = osuObject.StackedPosition;
+ Position = osuObject.StackedEndPosition;
Scale = new Vector2(osuObject.Scale);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 511cbc2347..9122f347d0 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
using osu.Game.Skinning;
@@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public SliderBall Ball { get; private set; }
public SkinnableDrawable Body { get; private set; }
- public override bool DisplayResult => false;
+ public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody;
@@ -249,7 +250,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (userTriggered || Time.Current < HitObject.EndTime)
return;
- ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
+ // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
+ // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
+ if (HitObject.OnlyJudgeNestedObjects)
+ {
+ ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
+ return;
+ }
+
+ // Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
+ ApplyResult(r =>
+ {
+ int totalTicks = NestedHitObjects.Count;
+ int hitTicks = NestedHitObjects.Count(h => h.IsHit);
+
+ if (hitTicks == totalTicks)
+ r.Type = HitResult.Great;
+ else if (hitTicks == 0)
+ r.Type = HitResult.Miss;
+ else
+ {
+ double hitFraction = (double)hitTicks / totalTicks;
+ r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh;
+ }
+ });
}
public override void PlaySamples()
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index acc95ab036..01c0d988ee 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -7,16 +7,27 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderHead : DrawableHitCircle
{
+ public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject;
+
[CanBeNull]
public Slider Slider => DrawableSlider?.HitObject;
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
+ public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
+
+ ///
+ /// Makes this track the follow circle when the start time is reached.
+ /// If false, this will be pinned to its initial position in the slider.
+ ///
+ public bool TrackFollowCircle = true;
+
private readonly IBindable pathVersion = new Bindable();
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
@@ -59,12 +70,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.Update();
Debug.Assert(Slider != null);
+ Debug.Assert(HitObject != null);
- double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
+ if (TrackFollowCircle)
+ {
+ double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
- //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
- if (!IsHit)
- Position = Slider.CurvePositionAt(completionProgress);
+ //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
+ if (!IsHit)
+ Position = Slider.CurvePositionAt(completionProgress);
+ }
+ }
+
+ protected override HitResult ResultFor(double timeOffset)
+ {
+ Debug.Assert(HitObject != null);
+
+ if (HitObject.JudgeAsNormalHitCircle)
+ return base.ResultFor(timeOffset);
+
+ // If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring.
+ var result = base.ResultFor(timeOffset);
+ return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
}
public Action OnShake;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index c58f703bef..d02376b6c3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -130,7 +130,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
if (tracking.NewValue)
{
- spinningSample?.Play(!spinningSample.IsPlaying);
+ if (!spinningSample.IsPlaying)
+ spinningSample?.Play();
spinningSample?.VolumeTo(1, 300);
}
else
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 15af141c99..22b64af3df 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@@ -25,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects
///
internal const float BASE_SCORING_DISTANCE = 100;
+ ///
+ /// Minimum preempt time at AR=10.
+ ///
+ public const double PREEMPT_MIN = 450;
+
public double TimePreempt = 600;
public double TimeFadeIn = 400;
@@ -112,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
- TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
- TimeFadeIn = 400; // as per osu-stable
+ TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
+
+ // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
+ // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.
+ // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good.
+ // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in.
+ TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN);
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 1670df24a8..e2b6c84896 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -114,8 +114,14 @@ namespace osu.Game.Rulesets.Osu.Objects
///
public double TickDistanceMultiplier = 1;
+ ///
+ /// Whether this 's judgement is fully handled by its nested s.
+ /// If false, this will be judged proportionally to the number of nested s hit.
+ ///
+ public bool OnlyJudgeNestedObjects = true;
+
[JsonIgnore]
- public HitCircle HeadCircle { get; protected set; }
+ public SliderHeadCircle HeadCircle { get; protected set; }
[JsonIgnore]
public SliderTailCircle TailCircle { get; protected set; }
@@ -140,7 +146,8 @@ namespace osu.Game.Rulesets.Osu.Objects
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
// For now, the samples are attached to and played by the slider itself at the correct end time.
- Samples = this.GetNodeSamples(repeatCount + 1);
+ // ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live).
+ Samples = this.GetNodeSamples(repeatCount + 1).ToArray();
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@@ -233,7 +240,7 @@ namespace osu.Game.Rulesets.Osu.Objects
HeadCircle.Samples = this.GetNodeSamples(0);
}
- public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
+ public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
index f6d46aeef5..5672283230 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
@@ -1,9 +1,19 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.Judgements;
+
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderHeadCircle : HitCircle
{
+ ///
+ /// Whether to treat this as a normal for judgement purposes.
+ /// If false, this will be judged as a instead.
+ ///
+ public bool JudgeAsNormalHitCircle = true;
+
+ public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement();
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
index a427ee1955..725dbe81fb 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
@@ -33,10 +33,5 @@ namespace osu.Game.Rulesets.Osu.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderTickJudgement();
-
- public class SliderTickJudgement : OsuJudgement
- {
- public override HitResult MaxResult => HitResult.LargeTickHit;
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs
index c8fe4f41ca..7314021a14 100644
--- a/osu.Game.Rulesets.Osu/OsuInputManager.cs
+++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu
protected override bool Handle(UIEvent e)
{
- if (e is MouseMoveEvent && !AllowUserCursorMovement) return false;
+ if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false;
return base.Handle(e);
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index cba0c5be14..18324a18a8 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -163,6 +163,7 @@ namespace osu.Game.Rulesets.Osu
{
new OsuModTarget(),
new OsuModDifficultyAdjust(),
+ new OsuModClassic()
};
case ModType.Automation:
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index 954a217473..693943a08a 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -6,10 +6,12 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Scoring;
@@ -33,11 +35,6 @@ namespace osu.Game.Rulesets.Osu.Replays
#region Constants
- ///
- /// The "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
- ///
- private readonly double reactionTime;
-
private readonly HitWindows defaultHitWindows;
///
@@ -49,12 +46,9 @@ namespace osu.Game.Rulesets.Osu.Replays
#region Construction / Initialisation
- public OsuAutoGenerator(IBeatmap beatmap)
- : base(beatmap)
+ public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList mods)
+ : base(beatmap, mods)
{
- // Already superhuman, but still somewhat realistic
- reactionTime = ApplyModsToRate(100);
-
defaultHitWindows = new OsuHitWindows();
defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
}
@@ -240,7 +234,7 @@ namespace osu.Game.Rulesets.Osu.Replays
OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[^1];
// Wait until Auto could "see and react" to the next note.
- double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime);
+ double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt));
if (waitTime > lastFrame.Time)
{
@@ -250,7 +244,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Vector2 lastPosition = lastFrame.Position;
- double timeDifference = ApplyModsToTime(h.StartTime - lastFrame.Time);
+ double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
// Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
if (timeDifference > 0 && // Sanity checks
@@ -258,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.Replays
timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
{
// Perform eased movement
- for (double time = lastFrame.Time + FrameDelay; time < h.StartTime; time += FrameDelay)
+ for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time))
{
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing);
AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions });
@@ -272,6 +266,14 @@ namespace osu.Game.Rulesets.Osu.Replays
}
}
+ ///
+ /// Calculates the "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
+ ///
+ ///
+ /// Already superhuman, but still somewhat realistic.
+ ///
+ private double getReactionTime(double timeInstant) => ApplyModsToRate(timeInstant, 100);
+
// Add frames to click the hitobject
private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection)
{
@@ -341,17 +343,23 @@ namespace osu.Game.Rulesets.Osu.Replays
float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X);
double t;
+ double previousFrame = h.StartTime;
- for (double j = h.StartTime + FrameDelay; j < spinner.EndTime; j += FrameDelay)
+ for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame))
{
- t = ApplyModsToTime(j - h.StartTime) * spinnerDirection;
+ t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection;
+ angle += (float)t / 20;
- Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS);
- AddFrameToReplay(new OsuReplayFrame((int)j, new Vector2(pos.X, pos.Y), action));
+ Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
+ AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action));
+
+ previousFrame = nextFrame;
}
- t = ApplyModsToTime(spinner.EndTime - h.StartTime) * spinnerDirection;
- Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS);
+ t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection;
+ angle += (float)t / 20;
+
+ Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action));
@@ -359,7 +367,7 @@ namespace osu.Game.Rulesets.Osu.Replays
break;
case Slider slider:
- for (double j = FrameDelay; j < slider.Duration; j += FrameDelay)
+ for (double j = GetFrameDelay(slider.StartTime); j < slider.Duration; j += GetFrameDelay(slider.StartTime + j))
{
Vector2 pos = slider.StackedPositionAt(j / slider.Duration);
AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action));
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
index 3356a0fbe0..1cb3208c30 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
@@ -5,7 +5,9 @@ using osuTK;
using osu.Game.Beatmaps;
using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Game.Replays;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
@@ -22,33 +24,61 @@ namespace osu.Game.Rulesets.Osu.Replays
public const float SPIN_RADIUS = 50;
- ///
- /// The time in ms between each ReplayFrame.
- ///
- protected readonly double FrameDelay;
-
#endregion
#region Construction / Initialisation
protected Replay Replay;
protected List Frames => Replay.Frames;
+ private readonly IReadOnlyList timeAffectingMods;
- protected OsuAutoGeneratorBase(IBeatmap beatmap)
+ protected OsuAutoGeneratorBase(IBeatmap beatmap, IReadOnlyList mods)
: base(beatmap)
{
Replay = new Replay();
- // We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps.
- FrameDelay = ApplyModsToRate(1000.0 / 60.0);
+ timeAffectingMods = mods.OfType().ToList();
}
#endregion
#region Utilities
- protected double ApplyModsToTime(double v) => v;
- protected double ApplyModsToRate(double v) => v;
+ ///
+ /// Returns the real duration of time between and
+ /// after applying rate-affecting mods.
+ ///
+ ///
+ /// This method should only be used when and are very close.
+ /// That is because the track rate might be changing with time,
+ /// and the method used here is a rough instantaneous approximation.
+ ///
+ /// The start time of the time delta, in original track time.
+ /// The end time of the time delta, in original track time.
+ protected double ApplyModsToTimeDelta(double startTime, double endTime)
+ {
+ double delta = endTime - startTime;
+
+ foreach (var mod in timeAffectingMods)
+ delta /= mod.ApplyToRate(startTime);
+
+ return delta;
+ }
+
+ protected double ApplyModsToRate(double time, double rate)
+ {
+ foreach (var mod in timeAffectingMods)
+ rate = mod.ApplyToRate(time, rate);
+ return rate;
+ }
+
+ ///
+ /// Calculates the interval after which the next should be generated,
+ /// in milliseconds.
+ ///
+ /// The time of the previous frame.
+ protected double GetFrameDelay(double time)
+ => ApplyModsToRate(time, 1000.0 / 60);
private class ReplayFrameComparer : IComparer
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
index e77c93c721..4dd7b2d69c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
+ private readonly Bindable configSnakingOut = new Bindable();
+
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableObject)
{
@@ -36,10 +38,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
- config?.BindWith(OsuRulesetSetting.SnakingOutSliders, SnakingOut);
+ config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);
+
+ SnakingOut.BindTo(configSnakingOut);
BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
+
+ drawableObject.HitObjectApplied += onHitObjectApplied;
+ }
+
+ private void onHitObjectApplied(DrawableHitObject obj)
+ {
+ var drawableSlider = (DrawableSlider)obj;
+ if (drawableSlider.HitObject == null)
+ return;
+
+ // When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way.
+ if (!drawableSlider.HeadCircle.TrackFollowCircle)
+ {
+ SnakingOut.UnbindFrom(configSnakingOut);
+ SnakingOut.Value = false;
+ }
}
private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour)
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
index a96beb66d4..82b677e12c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
@@ -31,6 +31,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
set => ball.Colour = value;
}
+ ///
+ /// Whether to track accurately to the visual size of this .
+ /// If false, tracking will be performed at the final scale at all times.
+ ///
+ public bool InputTracksVisualSize = true;
+
private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider;
private readonly Drawable ball;
@@ -94,7 +100,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
tracking = value;
- followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
+ if (InputTracksVisualSize)
+ followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
+ else
+ {
+ // We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration.
+ followCircle.ScaleTo(tracking ? 2.4f : 1f);
+ }
+
followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs
new file mode 100644
index 0000000000..83f205deac
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.UI
+{
+ ///
+ /// Ensures that s are hit in order of appearance. The classic note lock.
+ ///
+ /// Hits will be blocked until the previous s have been judged.
+ ///
+ ///
+ public class ObjectOrderedHitPolicy : IHitPolicy
+ {
+ public IHitObjectContainer HitObjectContainer { get; set; }
+
+ public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged);
+
+ public void HandleHit(DrawableHitObject hitObject)
+ {
+ }
+
+ private IEnumerable enumerateHitObjectsUpTo(double targetTime)
+ {
+ foreach (var obj in HitObjectContainer.AliveObjects)
+ {
+ if (obj.HitObject.StartTime >= targetTime)
+ yield break;
+
+ switch (obj)
+ {
+ case DrawableSpinner _:
+ continue;
+
+ case DrawableSlider slider:
+ yield return slider.HeadCircle;
+
+ break;
+
+ default:
+ yield return obj;
+
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index e085714265..b1069149f3 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer judgementLayer;
private readonly FollowPointRenderer followPoints;
- private readonly StartTimeOrderedHitPolicy hitPolicy;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -54,10 +53,9 @@ namespace osu.Game.Rulesets.Osu.UI
approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both },
};
- hitPolicy = new StartTimeOrderedHitPolicy { HitObjectContainer = HitObjectContainer };
+ HitPolicy = new StartTimeOrderedHitPolicy();
var hitWindows = new OsuHitWindows();
-
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded));
@@ -66,6 +64,18 @@ namespace osu.Game.Rulesets.Osu.UI
NewResult += onNewResult;
}
+ private IHitPolicy hitPolicy;
+
+ public IHitPolicy HitPolicy
+ {
+ get => hitPolicy;
+ set
+ {
+ hitPolicy = value ?? throw new ArgumentNullException(nameof(value));
+ hitPolicy.HitObjectContainer = HitObjectContainer;
+ }
+ }
+
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
{
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 2a5a2e2fdb..2b084f3bee 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
index 5b890b3d03..64e59b64d0 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModAutoplay : ModAutoplay
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs
index 71aa007d3b..00f0c8e321 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModCinema : ModCinema
{
- public override Score CreateReplayScore(IBeatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index d8e3100048..9f29675230 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -152,7 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
throw new ArgumentOutOfRangeException(nameof(component), $"Invalid component type: {component}");
}
- public override SampleChannel GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
+ public override Sample GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
public override IBindable GetConfig(TLookup lookup) => Source.GetConfig(lookup);
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index 600c820ce1..b80da928c8 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -21,6 +21,27 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(36, result.Links[0].Length);
}
+ [TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123#osu/456")]
+ [TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123#osu/456?whatever")]
+ [TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123/456")]
+ [TestCase(LinkAction.External, null, "https://dev.ppy.sh/beatmapsets/abc/def")]
+ [TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")]
+ [TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")]
+ [TestCase(LinkAction.External, null, "https://dev.ppy.sh/beatmapsets/abc")]
+ public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link)
+ {
+ MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
+
+ Message result = MessageFormatter.FormatMessage(new Message { Content = link });
+
+ Assert.AreEqual(result.Content, result.DisplayContent);
+ Assert.AreEqual(1, result.Links.Count);
+ Assert.AreEqual(expectedAction, result.Links[0].Action);
+ Assert.AreEqual(expectedArg, result.Links[0].Argument);
+ if (expectedAction == LinkAction.External)
+ Assert.AreEqual(link, result.Links[0].Url);
+ }
+
[Test]
public void TestMultipleComplexLinks()
{
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
index de46f9d1cf..3ded3009bd 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Gameplay
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
- public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable GetConfig(TLookup lookup)
{
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 38cb6729c3..7a0dd5b719 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Tests.Gameplay
public void TestRetrieveTopLevelSample()
{
ISkin skin = null;
- SampleChannel channel = null;
+ Sample channel = null;
AddStep("create skin", () => skin = new TestSkin("test-sample", this));
AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("test-sample")));
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Gameplay
public void TestRetrieveSampleInSubFolder()
{
ISkin skin = null;
- SampleChannel channel = null;
+ Sample channel = null;
AddStep("create skin", () => skin = new TestSkin("folder/test-sample", this));
AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("folder/test-sample")));
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
index a5c937119e..da004b9088 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
}
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException();
- public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
+ public Sample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
public IBindable GetConfig(TLookup lookup) => throw new NotSupportedException();
}
diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index aa6f66da81..ab24a72a12 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -68,12 +68,29 @@ namespace osu.Game.Tests.Online
Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
}
+ [Test]
+ public void TestDeserialiseDifficultyAdjustModWithExtendedLimits()
+ {
+ var apiMod = new APIMod(new TestModDifficultyAdjust
+ {
+ OverallDifficulty = { Value = 11 },
+ ExtendedLimits = { Value = true }
+ });
+
+ var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod));
+ var converted = (TestModDifficultyAdjust)deserialised.ToMod(new TestRuleset());
+
+ Assert.That(converted.ExtendedLimits.Value, Is.True);
+ Assert.That(converted.OverallDifficulty.Value, Is.EqualTo(11));
+ }
+
private class TestRuleset : Ruleset
{
public override IEnumerable GetModsFor(ModType type) => new Mod[]
{
new TestMod(),
new TestModTimeRamp(),
+ new TestModDifficultyAdjust()
};
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException();
@@ -135,5 +152,9 @@ namespace osu.Game.Tests.Online
Value = true
};
}
+
+ private class TestModDifficultyAdjust : ModDifficultyAdjust
+ {
+ }
}
}
diff --git a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
index 4294f89397..74db477cfc 100644
--- a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
@@ -68,6 +68,16 @@ namespace osu.Game.Tests.Online
Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
}
+ [Test]
+ public void TestDeserialiseEnumMod()
+ {
+ var apiMod = new APIMod(new TestModEnum { TestSetting = { Value = TestEnum.Value2 } });
+
+ var deserialized = MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(apiMod));
+
+ Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(1));
+ }
+
private class TestRuleset : Ruleset
{
public override IEnumerable GetModsFor(ModType type) => new Mod[]
@@ -135,5 +145,22 @@ namespace osu.Game.Tests.Online
Value = true
};
}
+
+ private class TestModEnum : Mod
+ {
+ public override string Name => "Test Mod";
+ public override string Acronym => "TM";
+ public override double ScoreMultiplier => 1;
+
+ [SettingSource("Test")]
+ public Bindable TestSetting { get; } = new Bindable();
+ }
+
+ private enum TestEnum
+ {
+ Value1 = 0,
+ Value2 = 1,
+ Value3 = 2
+ }
}
}
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index d3475de157..3ffb512b7f 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -165,10 +165,10 @@ namespace osu.Game.Tests.Online
{
}
- public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, CancellationToken cancellationToken = default)
+ public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
{
await AllowImport.Task;
- return await (CurrentImportTask = base.Import(item, archive, cancellationToken));
+ return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken));
}
}
diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
index 987a5812db..787f72ba79 100644
--- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
@@ -105,9 +105,9 @@ namespace osu.Game.Tests.Rulesets
IsDisposed = true;
}
- public SampleChannel Get(string name) => null;
+ public Sample Get(string name) => null;
- public Task GetAsync(string name) => null;
+ public Task GetAsync(string name) => null;
public Stream GetStream(string name) => null;
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index ad5b3ec0f6..414f7d3f88 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -219,7 +219,7 @@ namespace osu.Game.Tests.Skins
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => skin.GetTexture(componentName, wrapModeS, wrapModeT);
- public SampleChannel GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
+ public Sample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup);
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs
index f182023c0e..2abc8a8dec 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs
@@ -3,11 +3,11 @@
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Graphics.Audio;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Editing
{
@@ -19,14 +19,14 @@ namespace osu.Game.Tests.Visual.Editing
public void TestSlidingSampleStopsOnSeek()
{
DrawableSlider slider = null;
- DrawableSample[] loopingSamples = null;
- DrawableSample[] onceOffSamples = null;
+ PoolableSkinnableSample[] loopingSamples = null;
+ PoolableSkinnableSample[] onceOffSamples = null;
AddStep("get first slider", () =>
{
slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First();
- onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray();
- loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray();
+ onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray();
+ loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray();
});
AddStep("start playback", () => EditorClock.Start());
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
index 7c6a213fe2..6b3fc304e0 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Graphics.Audio;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
@@ -20,14 +19,14 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestAllSamplesStopDuringSeek()
{
DrawableSlider slider = null;
- DrawableSample[] samples = null;
+ PoolableSkinnableSample[] samples = null;
ISamplePlaybackDisabler sampleDisabler = null;
AddUntilStep("get variables", () =>
{
sampleDisabler = Player;
slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).FirstOrDefault();
- samples = slider?.ChildrenOfType().ToArray();
+ samples = slider?.ChildrenOfType().ToArray();
return slider != null;
});
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
index 3a71d4ca54..f94e122b30 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty());
- return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap));
+ return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap, Array.Empty()));
}
protected override void AddCheckSteps()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index bed48f3d86..44142b69d7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -298,7 +298,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
- public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
}
@@ -309,7 +309,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
- public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
}
@@ -321,7 +321,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
- public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index fc0cda2c1f..d688e9cb21 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -43,70 +43,60 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestStoppedSoundDoesntResumeAfterPause()
{
- DrawableSample sample = null;
AddStep("start sample with looping", () =>
{
- sample = skinnableSound.ChildrenOfType().First();
-
skinnableSound.Looping = true;
skinnableSound.Play();
});
- AddUntilStep("wait for sample to start playing", () => sample.Playing);
+ AddUntilStep("wait for sample to start playing", () => skinnableSound.IsPlaying);
AddStep("stop sample", () => skinnableSound.Stop());
- AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+ AddUntilStep("wait for sample to stop playing", () => !skinnableSound.IsPlaying);
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddWaitStep("wait a bit", 5);
- AddAssert("sample not playing", () => !sample.Playing);
+ AddAssert("sample not playing", () => !skinnableSound.IsPlaying);
}
[Test]
public void TestLoopingSoundResumesAfterPause()
{
- DrawableSample sample = null;
AddStep("start sample with looping", () =>
{
skinnableSound.Looping = true;
skinnableSound.Play();
- sample = skinnableSound.ChildrenOfType().First();
});
- AddUntilStep("wait for sample to start playing", () => sample.Playing);
+ AddUntilStep("wait for sample to start playing", () => skinnableSound.IsPlaying);
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
- AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+ AddUntilStep("wait for sample to stop playing", () => !skinnableSound.IsPlaying);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
- AddUntilStep("wait for sample to start playing", () => sample.Playing);
+ AddUntilStep("wait for sample to start playing", () => skinnableSound.IsPlaying);
}
[Test]
public void TestNonLoopingStopsWithPause()
{
- DrawableSample sample = null;
- AddStep("start sample", () =>
- {
- skinnableSound.Play();
- sample = skinnableSound.ChildrenOfType().First();
- });
+ AddStep("start sample", () => skinnableSound.Play());
- AddAssert("sample playing", () => sample.Playing);
+ AddAssert("sample playing", () => skinnableSound.IsPlaying);
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
- AddUntilStep("sample not playing", () => !sample.Playing);
+ AddUntilStep("sample not playing", () => !skinnableSound.IsPlaying);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
- AddAssert("sample not playing", () => !sample.Playing);
- AddAssert("sample not playing", () => !sample.Playing);
- AddAssert("sample not playing", () => !sample.Playing);
+ AddAssert("sample not playing", () => !skinnableSound.IsPlaying);
+ AddAssert("sample not playing", () => !skinnableSound.IsPlaying);
+ AddAssert("sample not playing", () => !skinnableSound.IsPlaying);
}
[Test]
@@ -119,10 +109,10 @@ namespace osu.Game.Tests.Visual.Gameplay
sample = skinnableSound.ChildrenOfType().Single();
});
- AddAssert("sample playing", () => sample.Playing);
+ AddAssert("sample playing", () => skinnableSound.IsPlaying);
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
- AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+ AddUntilStep("wait for sample to stop playing", () => !skinnableSound.IsPlaying);
AddStep("trigger skin change", () => skinSource.TriggerSourceChanged());
@@ -133,11 +123,11 @@ namespace osu.Game.Tests.Visual.Gameplay
return sample != oldSample;
});
- AddAssert("new sample stopped", () => !sample.Playing);
+ AddAssert("new sample stopped", () => !skinnableSound.IsPlaying);
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
AddWaitStep("wait a bit", 5);
- AddAssert("new sample not played", () => !sample.Playing);
+ AddAssert("new sample not played", () => !skinnableSound.IsPlaying);
}
[Cached(typeof(ISkinSource))]
@@ -155,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
- public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
+ public Sample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup);
public void TriggerSourceChanged()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
index 26524f07da..4a0e1282c4 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -244,11 +243,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
- protected override Task Connect()
- {
- return Task.CompletedTask;
- }
-
public void StartPlay(int beatmapId)
{
this.beatmapId = beatmapId;
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 279dcfa584..5682fd5c3c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -69,6 +69,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("last room joined", () => RoomManager.Rooms.Last().Status.Value is JoinedRoomStatus);
}
+ [Test]
+ public void TestClickDeselection()
+ {
+ AddRooms(1);
+
+ AddAssert("no selection", () => checkRoomSelected(null));
+
+ press(Key.Down);
+ AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
+
+ AddStep("click away", () => InputManager.Click(MouseButton.Left));
+ AddAssert("no selection", () => checkRoomSelected(null));
+ }
+
private void press(Key down)
{
AddStep($"press {down}", () => InputManager.Key(down));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index d016accc25..aab69d687a 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -163,8 +162,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty()));
}
}
-
- protected override Task Connect() => Task.CompletedTask;
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index e2c98c0aad..0f7a9b442d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -7,7 +7,10 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
@@ -19,16 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
{
[SetUp]
- public new void Setup() => Schedule(() =>
- {
- Child = new ParticipantsList
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Y,
- Size = new Vector2(380, 0.7f)
- };
- });
+ public new void Setup() => Schedule(createNewParticipantsList);
[Test]
public void TestAddUser()
@@ -75,6 +69,50 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.User == secondUser);
}
+ [Test]
+ public void TestGameStateHasPriorityOverDownloadState()
+ {
+ AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
+ checkProgressBarVisibility(true);
+
+ AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Results));
+ checkProgressBarVisibility(false);
+ AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent);
+
+ AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Idle));
+ checkProgressBarVisibility(true);
+ }
+
+ [Test]
+ public void TestCorrectInitialState()
+ {
+ AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
+ AddStep("recreate list", createNewParticipantsList);
+ checkProgressBarVisibility(true);
+ }
+
+ [Test]
+ public void TestBeatmapDownloadingStates()
+ {
+ AddStep("set to no map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
+ AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
+
+ checkProgressBarVisibility(true);
+
+ AddRepeatStep("increment progress", () =>
+ {
+ var progress = this.ChildrenOfType().Single().User.BeatmapAvailability.DownloadProgress ?? 0;
+ Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f)));
+ }, 25);
+
+ AddAssert("progress bar increased", () => this.ChildrenOfType().Single().Current.Value > 0);
+
+ AddStep("set to importing map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Importing()));
+ checkProgressBarVisibility(false);
+
+ AddStep("set to available", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()));
+ }
+
[Test]
public void TestToggleReadyState()
{
@@ -122,6 +160,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1));
+
+ if (RNG.NextBool())
+ {
+ var beatmapState = (DownloadState)RNG.Next(0, (int)DownloadState.LocallyAvailable + 1);
+
+ switch (beatmapState)
+ {
+ case DownloadState.NotDownloaded:
+ Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.NotDownloaded());
+ break;
+
+ case DownloadState.Downloading:
+ Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Downloading(RNG.NextSingle()));
+ break;
+
+ case DownloadState.Importing:
+ Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Importing());
+ break;
+ }
+ }
}
});
}
@@ -152,5 +210,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep($"set state: {state}", () => Client.ChangeUserState(0, state));
}
}
+
+ private void createNewParticipantsList()
+ {
+ Child = new ParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Size = new Vector2(380, 0.7f) };
+ }
+
+ private void checkProgressBarVisibility(bool visible) =>
+ AddUntilStep($"progress bar {(visible ? "is" : "is not")}visible", () =>
+ this.ChildrenOfType().Single().IsPresent == visible);
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
index 1d13c6229c..7d83ba569d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
@@ -87,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault();
+ SelectedMods.Value = Array.Empty();
});
AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect()));
@@ -143,7 +144,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// Tests that the same instances are not shared between two playlist items.
///
[Test]
- [Ignore("Temporarily disabled due to a non-trivial test failure")]
public void TestNewItemHasNewModInstances()
{
AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
index 9e69530a77..74f53ebdca 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs
@@ -103,26 +103,26 @@ namespace osu.Game.Tests.Visual.Online
private void testLinksGeneral()
{
addMessageWithChecks("test!");
- addMessageWithChecks("osu.ppy.sh!");
- addMessageWithChecks("https://osu.ppy.sh!", 1, expectedActions: LinkAction.External);
+ addMessageWithChecks("dev.ppy.sh!");
+ addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp);
addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("(osu forums)[https://osu.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("[https://osu.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("[osu forums](https://osu.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("[https://osu.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("is now listening to [https://osu.ppy.sh/s/93523 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmapSet);
- addMessageWithChecks("is now playing [https://osu.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap);
- addMessageWithChecks("Let's (try)[https://osu.ppy.sh/home] [https://osu.ppy.sh/b/252238 multiple links] https://osu.ppy.sh/home", 3,
+ addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
+ addMessageWithChecks("[https://dev.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External);
+ addMessageWithChecks("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External);
+ addMessageWithChecks("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External);
+ addMessageWithChecks("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmapSet);
+ addMessageWithChecks("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap);
+ addMessageWithChecks("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", 3,
expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External });
- addMessageWithChecks("[https://osu.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://osu.ppy.sh/home)", 1, expectedActions: LinkAction.External);
- addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://osu.ppy.sh/home] and [[also a rogue wiki link]]", 2, expectedActions: new[] { LinkAction.External, LinkAction.External });
+ addMessageWithChecks("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External);
+ addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", 1, expectedActions: LinkAction.External);
+ addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", 2, expectedActions: new[] { LinkAction.External, LinkAction.External });
// note that there's 0 links here (they get removed if a channel is not found)
addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).");
addMessageWithChecks("I am important!", 0, false, true);
addMessageWithChecks("feels important", 0, true, true);
- addMessageWithChecks("likes to post this [https://osu.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
+ addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
addMessageWithChecks("Join my [#english](osu://chan/#english).", 1, expectedActions: LinkAction.OpenChannel);
@@ -136,9 +136,9 @@ namespace osu.Game.Tests.Visual.Online
int echoCounter = 0;
addEchoWithWait("sent!", "received!");
- addEchoWithWait("https://osu.ppy.sh/home", null, 500);
- addEchoWithWait("[https://osu.ppy.sh/forum let's try multiple words too!]");
- addEchoWithWait("(long loading times! clickable while loading?)[https://osu.ppy.sh/home]", null, 5000);
+ addEchoWithWait("https://dev.ppy.sh/home", null, 500);
+ addEchoWithWait("[https://dev.ppy.sh/forum let's try multiple words too!]");
+ addEchoWithWait("(long loading times! clickable while loading?)[https://dev.ppy.sh/home]", null, 5000);
void addEchoWithWait(string text, string completeText = null, double delay = 250)
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs
index 8f20bcdcc1..dc468bb62d 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
private class TestFullscreenOverlay : FullscreenOverlay
{
public TestFullscreenOverlay()
- : base(OverlayColourScheme.Pink, null)
+ : base(OverlayColourScheme.Pink)
{
Children = new Drawable[]
{
@@ -52,6 +52,17 @@ namespace osu.Game.Tests.Visual.Online
},
};
}
+
+ protected override OverlayHeader CreateHeader() => new TestHeader();
+
+ internal class TestHeader : OverlayHeader
+ {
+ protected override OverlayTitle CreateTitle() => new TestTitle();
+
+ internal class TestTitle : OverlayTitle
+ {
+ }
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs
index 626f545b91..aff510dd95 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Online
Add(rankingsOverlay = new TestRankingsOverlay
{
Country = { BindTarget = countryBindable },
- Scope = { BindTarget = scope },
+ Header = { Current = { BindTarget = scope } },
});
}
@@ -65,8 +65,6 @@ namespace osu.Game.Tests.Visual.Online
private class TestRankingsOverlay : RankingsOverlay
{
public new Bindable Country => base.Country;
-
- public new Bindable Scope => base.Scope;
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 37ebc72984..2885dbee00 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -39,7 +39,11 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[SetUp]
- public void SetUp() => Schedule(() => createDisplay(() => new TestModSelectOverlay()));
+ public void SetUp() => Schedule(() =>
+ {
+ SelectedMods.Value = Array.Empty();
+ createDisplay(() => new TestModSelectOverlay());
+ });
[SetUpSteps]
public void SetUpSteps()
@@ -47,6 +51,24 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("show", () => modSelect.Show());
}
+ [Test]
+ public void TestSettingsResetOnDeselection()
+ {
+ var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } };
+
+ changeRuleset(0);
+
+ AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; });
+
+ AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
+
+ AddStep("deselect", () => modSelect.DeselectAllButton.Click());
+ AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
+
+ AddStep("reselect", () => modSelect.GetModButton(osuModDoubleTime).Click());
+ AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
+ }
+
[Test]
public void TestAnimationFlushOnClose()
{
@@ -152,6 +174,45 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
+ [Test]
+ public void TestSettingsAreRetainedOnReload()
+ {
+ changeRuleset(0);
+
+ AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
+
+ AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
+
+ AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay()));
+
+ AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
+ }
+
+ [Test]
+ public void TestExternallySetModIsReplacedByOverlayInstance()
+ {
+ Mod external = new OsuModDoubleTime();
+ Mod overlayButtonMod = null;
+
+ changeRuleset(0);
+
+ AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
+
+ AddAssert("ensure button is selected", () =>
+ {
+ var button = modSelect.GetModButton(SelectedMods.Value.Single());
+ overlayButtonMod = button.SelectedMod;
+ return overlayButtonMod.GetType() == external.GetType();
+ });
+
+ // Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own
+ AddAssert("mod instance doesn't match", () => external != overlayButtonMod);
+
+ AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1);
+ AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Contains(overlayButtonMod));
+ AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Contains(external));
+ }
+
[Test]
public void TestNonStacked()
{
@@ -313,7 +374,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createDisplay(Func createOverlayFunc)
{
- SelectedMods.Value = Array.Empty();
Children = new Drawable[]
{
modSelect = createOverlayFunc().With(d =>
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
index 43ba23e6c6..d0f6f3fe47 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
@@ -105,6 +105,15 @@ namespace osu.Game.Tests.Visual.UserInterface
checkDisplayedCount(3);
}
+ [Test]
+ public void TestError()
+ {
+ setState(Visibility.Visible);
+ AddStep(@"error #1", sendErrorNotification);
+ AddAssert("Is visible", () => notificationOverlay.State.Value == Visibility.Visible);
+ checkDisplayedCount(1);
+ }
+
[Test]
public void TestSpam()
{
@@ -179,7 +188,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void sendBarrage()
{
- switch (RNG.Next(0, 4))
+ switch (RNG.Next(0, 5))
{
case 0:
sendHelloNotification();
@@ -196,6 +205,10 @@ namespace osu.Game.Tests.Visual.UserInterface
case 3:
sendDownloadProgress();
break;
+
+ case 4:
+ sendErrorNotification();
+ break;
}
}
@@ -214,6 +227,11 @@ namespace osu.Game.Tests.Visual.UserInterface
notificationOverlay.Post(new BackgroundNotification { Text = @"Welcome to osu!. Enjoy your stay!" });
}
+ private void sendErrorNotification()
+ {
+ notificationOverlay.Post(new SimpleErrorNotification { Text = @"Rut roh!. Something went wrong!" });
+ }
+
private void sendManyNotifications()
{
for (int i = 0; i < 10; i++)
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index d29ed94b5f..7e3868bd3b 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs
index f7ad757926..50bdcd86c5 100644
--- a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs
+++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs
@@ -13,15 +13,18 @@ namespace osu.Game.Tournament.Tests
{
base.LoadComplete();
- LoadComponentAsync(new Background("Menu/menu-background-0")
+ BracketLoadTask.ContinueWith(_ => Schedule(() =>
{
- Colour = OsuColour.Gray(0.5f),
- Depth = 10
- }, AddInternal);
+ LoadComponentAsync(new Background("Menu/menu-background-0")
+ {
+ Colour = OsuColour.Gray(0.5f),
+ Depth = 10
+ }, AddInternal);
- // Have to construct this here, rather than in the constructor, because
- // we depend on some dependencies to be loaded within OsuGameBase.load().
- Add(new TestBrowser());
+ // Have to construct this here, rather than in the constructor, because
+ // we depend on some dependencies to be loaded within OsuGameBase.load().
+ Add(new TestBrowser());
+ }));
}
}
}
diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs
index d22da25f9d..47d2160561 100644
--- a/osu.Game.Tournament.Tests/TournamentTestScene.cs
+++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Framework.Testing;
@@ -154,13 +155,22 @@ namespace osu.Game.Tournament.Tests
protected override void LoadAsyncComplete()
{
- // this has to be run here rather than LoadComplete because
- // TestScene.cs is checking the IsLoaded state (on another thread) and expects
- // the runner to be loaded at that point.
- Add(runner = new TestSceneTestRunner.TestRunner());
+ BracketLoadTask.ContinueWith(_ => Schedule(() =>
+ {
+ // this has to be run here rather than LoadComplete because
+ // TestScene.cs is checking the IsLoaded state (on another thread) and expects
+ // the runner to be loaded at that point.
+ Add(runner = new TestSceneTestRunner.TestRunner());
+ }));
}
- public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test);
+ public void RunTestBlocking(TestScene test)
+ {
+ while (runner?.IsLoaded != true && Host.ExecutionState == ExecutionState.Running)
+ Thread.Sleep(10);
+
+ runner?.RunTestBlocking(test);
+ }
}
}
}
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 185b35e40d..77ae06d89c 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs
index bbe4a53d8f..fadb821bef 100644
--- a/osu.Game.Tournament/TournamentGame.cs
+++ b/osu.Game.Tournament/TournamentGame.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Colour;
using osu.Game.Graphics.Cursor;
using osu.Game.Tournament.Models;
using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@@ -32,25 +33,24 @@ namespace osu.Game.Tournament
private Drawable heightWarning;
private Bindable windowSize;
private Bindable windowMode;
+ private LoadingSpinner loadingSpinner;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager frameworkConfig)
{
windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize);
- windowSize.BindValueChanged(size => ScheduleAfterChildren(() =>
- {
- var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1;
-
- heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
- }), true);
-
windowMode = frameworkConfig.GetBindable(FrameworkSetting.WindowMode);
- windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
- {
- windowMode.Value = WindowMode.Windowed;
- }), true);
- AddRange(new[]
+ Add(loadingSpinner = new LoadingSpinner(true, true)
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Margin = new MarginPadding(40),
+ });
+
+ loadingSpinner.Show();
+
+ BracketLoadTask.ContinueWith(_ => LoadComponentsAsync(new[]
{
new Container
{
@@ -93,7 +93,24 @@ namespace osu.Game.Tournament
RelativeSizeAxes = Axes.Both,
Child = new TournamentSceneManager()
}
- });
+ }, drawables =>
+ {
+ loadingSpinner.Hide();
+ loadingSpinner.Expire();
+
+ AddRange(drawables);
+
+ windowSize.BindValueChanged(size => ScheduleAfterChildren(() =>
+ {
+ var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1;
+ heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
+ }), true);
+
+ windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
+ {
+ windowMode.Value = WindowMode.Windowed;
+ }), true);
+ }));
}
}
}
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index 97c950261b..ffda101ee0 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
@@ -29,6 +30,10 @@ namespace osu.Game.Tournament
private DependencyContainer dependencies;
private FileBasedIPC ipc;
+ protected Task BracketLoadTask => taskCompletionSource.Task;
+
+ private readonly TaskCompletionSource taskCompletionSource = new TaskCompletionSource();
+
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
return dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@@ -46,14 +51,9 @@ namespace osu.Game.Tournament
Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage)));
- readBracket();
-
- ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value);
-
dependencies.CacheAs(new StableInfo(storage));
- dependencies.CacheAs(ipc = new FileBasedIPC());
- Add(ipc);
+ Task.Run(readBracket);
}
private void readBracket()
@@ -68,10 +68,6 @@ namespace osu.Game.Tournament
ladder ??= new LadderInfo();
ladder.Ruleset.Value ??= RulesetStore.AvailableRulesets.First();
- Ruleset.BindTo(ladder.Ruleset);
-
- dependencies.Cache(ladder);
-
bool addedInfo = false;
// assign teams
@@ -127,6 +123,19 @@ namespace osu.Game.Tournament
if (addedInfo)
SaveChanges();
+
+ ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value);
+
+ Schedule(() =>
+ {
+ Ruleset.BindTo(ladder.Ruleset);
+
+ dependencies.Cache(ladder);
+ dependencies.CacheAs(ipc = new FileBasedIPC());
+ Add(ipc);
+
+ taskCompletionSource.SetResult(true);
+ });
}
///
@@ -143,7 +152,7 @@ namespace osu.Game.Tournament
{
if (string.IsNullOrEmpty(p.Username) || p.Statistics == null)
{
- PopulateUser(p);
+ PopulateUser(p, immediate: true);
addedInfo = true;
}
}
@@ -202,12 +211,14 @@ namespace osu.Game.Tournament
return addedInfo;
}
- public void PopulateUser(User user, Action success = null, Action failure = null)
+ public void PopulateUser(User user, Action success = null, Action failure = null, bool immediate = false)
{
var req = new GetUserRequest(user.Id, Ruleset.Value);
req.Success += res =>
{
+ user.Id = res.Id;
+
user.Username = res.Username;
user.Statistics = res.Statistics;
user.Country = res.Country;
@@ -222,7 +233,10 @@ namespace osu.Game.Tournament
failure?.Invoke();
};
- API.Queue(req);
+ if (immediate)
+ API.Perform(req);
+ else
+ API.Queue(req);
}
protected override void LoadComplete()
diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs
index 8d02af6574..d88fd1e62b 100644
--- a/osu.Game/Audio/PreviewTrackManager.cs
+++ b/osu.Game/Audio/PreviewTrackManager.cs
@@ -27,6 +27,8 @@ namespace osu.Game.Audio
protected TrackManagerPreviewTrack CurrentTrack;
+ private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(OsuGameBase.GLOBAL_TRACK_VOLUME_ADJUST);
+
[BackgroundDependencyLoader]
private void load()
{
@@ -35,6 +37,7 @@ namespace osu.Game.Audio
trackStore = new PreviewTrackStore(new OnlineStore());
audio.AddItem(trackStore);
+ trackStore.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust);
trackStore.AddAdjustment(AdjustableProperty.Volume, audio.VolumeTrack);
}
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index b934ac556d..3c6a6ba302 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -64,7 +64,9 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" };
- protected override string ImportFromStablePath => "Songs";
+ protected override string ImportFromStablePath => ".";
+
+ protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage();
private readonly RulesetStore rulesets;
private readonly BeatmapStore beatmaps;
diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs
index 39b3c23ddd..367f612dc8 100644
--- a/osu.Game/Beatmaps/BeatmapMetadata.cs
+++ b/osu.Game/Beatmaps/BeatmapMetadata.cs
@@ -51,7 +51,12 @@ namespace osu.Game.Beatmaps
[JsonProperty(@"tags")]
public string Tags { get; set; }
+ ///
+ /// The time in milliseconds to begin playing the track for preview purposes.
+ /// If -1, the track should begin playing at 40% of its length.
+ ///
public int PreviewTime { get; set; }
+
public string AudioFile { get; set; }
public string BackgroundFile { get; set; }
diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
index eb05cbaf85..3206f7b3ab 100644
--- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
@@ -34,8 +34,8 @@ namespace osu.Game.Beatmaps.Drawables
///
protected virtual double UnloadDelay => 10000;
- protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad)
- => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay);
+ protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) =>
+ new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay) { RelativeSizeAxes = Axes.Both };
protected override double TransformDuration => 400;
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 30382c444f..aab8ff6bd6 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -266,6 +266,26 @@ namespace osu.Game.Beatmaps
[NotNull]
public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000);
+ ///
+ /// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
+ ///
+ public void PrepareTrackForPreviewLooping()
+ {
+ Track.Looping = true;
+ Track.RestartPoint = Metadata.PreviewTime;
+
+ if (Track.RestartPoint == -1)
+ {
+ if (!Track.IsLoaded)
+ {
+ // force length to be populated (https://github.com/ppy/osu-framework/issues/4202)
+ Track.Seek(Track.CurrentTime);
+ }
+
+ Track.RestartPoint = 0.4f * Track.Length;
+ }
+ }
+
///
/// Transfer a valid audio track into this working beatmap. Used as an optimisation to avoid reload / track swap
/// across difficulties in the same beatmap set.
diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs
index ec0e9d5a89..bb743d4ccc 100644
--- a/osu.Game/Collections/CollectionFilterDropdown.cs
+++ b/osu.Game/Collections/CollectionFilterDropdown.cs
@@ -29,6 +29,14 @@ namespace osu.Game.Collections
///
protected virtual bool ShowManageCollectionsItem => true;
+ private readonly BindableWithCurrent current = new BindableWithCurrent();
+
+ public new Bindable Current
+ {
+ get => current.Current;
+ set => current.Current = value;
+ }
+
private readonly IBindableList collections = new BindableList();
private readonly IBindableList beatmaps = new BindableList();
private readonly BindableList filters = new BindableList();
@@ -36,25 +44,28 @@ namespace osu.Game.Collections
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
+ [Resolved(CanBeNull = true)]
+ private CollectionManager collectionManager { get; set; }
+
public CollectionFilterDropdown()
{
ItemSource = filters;
- }
-
- [BackgroundDependencyLoader(permitNulls: true)]
- private void load([CanBeNull] CollectionManager collectionManager)
- {
- if (collectionManager != null)
- collections.BindTo(collectionManager.Collections);
-
- collections.CollectionChanged += (_, __) => collectionsChanged();
- collectionsChanged();
+ Current.Value = new AllBeatmapsCollectionFilterMenuItem();
}
protected override void LoadComplete()
{
base.LoadComplete();
+ if (collectionManager != null)
+ collections.BindTo(collectionManager.Collections);
+
+ // Dropdown has logic which triggers a change on the bindable with every change to the contained items.
+ // This is not desirable here, as it leads to multiple filter operations running even though nothing has changed.
+ // An extra bindable is enough to subvert this behaviour.
+ base.Current = Current;
+
+ collections.BindCollectionChanged((_, __) => collectionsChanged(), true);
Current.BindValueChanged(filterChanged, true);
}
diff --git a/osu.Game/Collections/CollectionFilterMenuItem.cs b/osu.Game/Collections/CollectionFilterMenuItem.cs
index 4a489d2945..fe79358223 100644
--- a/osu.Game/Collections/CollectionFilterMenuItem.cs
+++ b/osu.Game/Collections/CollectionFilterMenuItem.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using JetBrains.Annotations;
using osu.Framework.Bindables;
@@ -9,7 +10,7 @@ namespace osu.Game.Collections
///
/// A filter.
///
- public class CollectionFilterMenuItem
+ public class CollectionFilterMenuItem : IEquatable
{
///
/// The collection to filter beatmaps from.
@@ -33,6 +34,11 @@ namespace osu.Game.Collections
Collection = collection;
CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable("All beatmaps");
}
+
+ public bool Equals(CollectionFilterMenuItem other)
+ => other != null && CollectionName.Value == other.CollectionName.Value;
+
+ public override int GetHashCode() => CollectionName.Value.GetHashCode();
}
public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem
diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index 569ac749a4..a65d9a415d 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -139,35 +139,43 @@ namespace osu.Game.Collections
PostNotification?.Invoke(notification);
var collection = readCollections(stream, notification);
- bool importCompleted = false;
-
- Schedule(() =>
- {
- importCollections(collection);
- importCompleted = true;
- });
-
- while (!IsDisposed && !importCompleted)
- await Task.Delay(10);
+ await importCollections(collection);
notification.CompletionText = $"Imported {collection.Count} collections";
notification.State = ProgressNotificationState.Completed;
}
- private void importCollections(List newCollections)
+ private Task importCollections(List newCollections)
{
- foreach (var newCol in newCollections)
- {
- var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name);
- if (existing == null)
- Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
+ var tcs = new TaskCompletionSource();
- foreach (var newBeatmap in newCol.Beatmaps)
+ Schedule(() =>
+ {
+ try
{
- if (!existing.Beatmaps.Contains(newBeatmap))
- existing.Beatmaps.Add(newBeatmap);
+ foreach (var newCol in newCollections)
+ {
+ var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name);
+ if (existing == null)
+ Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
+
+ foreach (var newBeatmap in newCol.Beatmaps)
+ {
+ if (!existing.Beatmaps.Contains(newBeatmap))
+ existing.Beatmaps.Add(newBeatmap);
+ }
+ }
+
+ tcs.SetResult(true);
}
- }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Failed to import collection.");
+ tcs.SetException(e);
+ }
+ });
+
+ return tcs.Task;
}
private List readCollections(Stream stream, ProgressNotification notification = null)
diff --git a/osu.Game/Configuration/ModSettingChangeTracker.cs b/osu.Game/Configuration/ModSettingChangeTracker.cs
new file mode 100644
index 0000000000..e2ade7dc6a
--- /dev/null
+++ b/osu.Game/Configuration/ModSettingChangeTracker.cs
@@ -0,0 +1,52 @@
+// 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.Game.Overlays.Settings;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Configuration
+{
+ ///
+ /// A helper class for tracking changes to the settings of a set of s.
+ ///
+ ///
+ /// Ensure to dispose when usage is finished.
+ ///
+ public class ModSettingChangeTracker : IDisposable
+ {
+ ///
+ /// Notifies that the setting of a has changed.
+ ///
+ public Action SettingChanged;
+
+ private readonly List settings = new List();
+
+ ///
+ /// Creates a new for a set of s.
+ ///
+ /// The set of s whose settings need to be tracked.
+ public ModSettingChangeTracker(IEnumerable mods)
+ {
+ foreach (var mod in mods)
+ {
+ foreach (var setting in mod.CreateSettingsControls().OfType())
+ {
+ setting.SettingChanged += () => SettingChanged?.Invoke(mod);
+ settings.Add(setting);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ SettingChanged = null;
+
+ foreach (var r in settings)
+ r.Dispose();
+ settings.Clear();
+ }
+ }
+}
diff --git a/osu.Game/Configuration/ScoreMeterType.cs b/osu.Game/Configuration/ScoreMeterType.cs
index b9499c758e..ddbd2327c2 100644
--- a/osu.Game/Configuration/ScoreMeterType.cs
+++ b/osu.Game/Configuration/ScoreMeterType.cs
@@ -16,12 +16,12 @@ namespace osu.Game.Configuration
[Description("Hit Error (right)")]
HitErrorRight,
- [Description("Hit Error (bottom)")]
- HitErrorBottom,
-
[Description("Hit Error (left+right)")]
HitErrorBoth,
+ [Description("Hit Error (bottom)")]
+ HitErrorBottom,
+
[Description("Colour (left)")]
ColourLeft,
diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index 50069be4b2..70d67aaaa0 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -57,6 +57,7 @@ namespace osu.Game.Configuration
yield return new SettingsSlider
{
LabelText = attr.Label,
+ TooltipText = attr.Description,
Current = bNumber,
KeyboardStep = 0.1f,
};
@@ -67,6 +68,7 @@ namespace osu.Game.Configuration
yield return new SettingsSlider
{
LabelText = attr.Label,
+ TooltipText = attr.Description,
Current = bNumber,
KeyboardStep = 0.1f,
};
@@ -77,6 +79,7 @@ namespace osu.Game.Configuration
yield return new SettingsSlider
{
LabelText = attr.Label,
+ TooltipText = attr.Description,
Current = bNumber
};
@@ -86,6 +89,7 @@ namespace osu.Game.Configuration
yield return new SettingsCheckbox
{
LabelText = attr.Label,
+ TooltipText = attr.Description,
Current = bBool
};
@@ -95,6 +99,7 @@ namespace osu.Game.Configuration
yield return new SettingsTextBox
{
LabelText = attr.Label,
+ TooltipText = attr.Description,
Current = bString
};
@@ -105,6 +110,7 @@ namespace osu.Game.Configuration
var dropdown = (Drawable)Activator.CreateInstance(dropdownType);
dropdownType.GetProperty(nameof(SettingsDropdown
/// The containing data about the to import.
+ /// Whether this is a low priority import.
/// An optional cancellation token.
/// The imported model, if successful.
- internal async Task Import(ImportTask task, CancellationToken cancellationToken = default)
+ internal async Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
TModel import;
using (ArchiveReader reader = task.GetReader())
- import = await Import(reader, cancellationToken);
+ import = await Import(reader, lowPriority, cancellationToken);
// We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with items from default storage.
@@ -226,11 +244,12 @@ namespace osu.Game.Database
public Action> PresentImport;
///
- /// Import an item from an .
+ /// Silently import an item from an .
///
/// The archive to be imported.
+ /// Whether this is a low priority import.
/// An optional cancellation token.
- public Task Import(ArchiveReader archive, CancellationToken cancellationToken = default)
+ public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -253,7 +272,7 @@ namespace osu.Game.Database
return null;
}
- return Import(model, archive, cancellationToken);
+ return Import(model, archive, lowPriority, cancellationToken);
}
///
@@ -303,12 +322,13 @@ namespace osu.Game.Database
}
///
- /// Import an item from a .
+ /// Silently import an item from a .
///
/// The model to be imported.
/// An optional archive to use for model population.
+ /// Whether this is a low priority import.
/// An optional cancellation token.
- public virtual async Task Import(TModel item, ArchiveReader archive = null, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () =>
+ public virtual async Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
@@ -383,7 +403,7 @@ namespace osu.Game.Database
flushEvents(true);
return item;
- }, cancellationToken, TaskCreationOptions.HideScheduler, import_scheduler).Unwrap();
+ }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap();
///
/// Exports an item to a legacy (.zip based) package.
@@ -625,7 +645,7 @@ namespace osu.Game.Database
///
/// Set a storage with access to an osu-stable install for import purposes.
///
- public Func GetStableStorage { private get; set; }
+ public Func GetStableStorage { private get; set; }
///
/// Denotes whether an osu-stable installation is present to perform automated imports from.
@@ -638,9 +658,10 @@ namespace osu.Game.Database
protected virtual string ImportFromStablePath => null;
///
- /// Select paths to import from stable. Default implementation iterates all directories in .
+ /// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in .
///
- protected virtual IEnumerable GetStableImportPaths(Storage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath);
+ protected virtual IEnumerable GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath)
+ .Select(path => storage.GetFullPath(path));
///
/// Whether this specified path should be removed after successful import.
@@ -654,24 +675,33 @@ namespace osu.Game.Database
///
public Task ImportFromStableAsync()
{
- var stable = GetStableStorage?.Invoke();
+ var stableStorage = GetStableStorage?.Invoke();
- if (stable == null)
+ if (stableStorage == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
- if (!stable.ExistsDirectory(ImportFromStablePath))
+ var storage = PrepareStableStorage(stableStorage);
+
+ if (!storage.ExistsDirectory(ImportFromStablePath))
{
// This handles situations like when the user does not have a Skins folder
Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
- return Task.Run(async () => await Import(GetStableImportPaths(GetStableStorage()).Select(f => stable.GetFullPath(f)).ToArray()));
+ return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()));
}
+ ///
+ /// Run any required traversal operations on the stable storage location before performing operations.
+ ///
+ /// The stable storage.
+ /// The usable storage. Return the unchanged if no traversal is required.
+ protected virtual Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage;
+
#endregion
///
diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
index e3a9a5fe9d..914c8ff78d 100644
--- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs
+++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
@@ -38,7 +38,12 @@ namespace osu.Game.Graphics.Containers
foreach (var link in links)
{
AddText(text[previousLinkEnd..link.Index]);
- AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url);
+
+ string displayText = text.Substring(link.Index, link.Length);
+ string linkArgument = link.Argument ?? link.Url;
+ string tooltip = displayText == link.Url ? null : link.Url;
+
+ AddLink(displayText, link.Action, linkArgument, tooltip);
previousLinkEnd = link.Index + link.Length;
}
@@ -52,7 +57,7 @@ namespace osu.Game.Graphics.Containers
=> createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, null), tooltipText, action);
public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action creationParameters = null)
- => createLink(AddText(text, creationParameters), new LinkDetails(action, argument), null);
+ => createLink(AddText(text, creationParameters), new LinkDetails(action, argument), tooltipText);
public void AddLink(IEnumerable text, LinkAction action = LinkAction.External, string linkArgument = null, string tooltipText = null)
{
diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
index 41fd37a0d7..fbf2ffd4bd 100644
--- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
@@ -18,8 +18,10 @@ namespace osu.Game.Graphics.Containers
[Cached(typeof(IPreviewTrackOwner))]
public abstract class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler
{
- private SampleChannel samplePopIn;
- private SampleChannel samplePopOut;
+ private Sample samplePopIn;
+ private Sample samplePopOut;
+ protected virtual string PopInSampleName => "UI/overlay-pop-in";
+ protected virtual string PopOutSampleName => "UI/overlay-pop-out";
protected override bool BlockNonPositionalInput => true;
@@ -40,8 +42,8 @@ namespace osu.Game.Graphics.Containers
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio)
{
- samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in");
- samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out");
+ samplePopIn = audio.Samples.Get(PopInSampleName);
+ samplePopOut = audio.Samples.Get(PopOutSampleName);
}
protected override void LoadComplete()
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index 53ee711626..f7914cbbca 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Graphics
[Resolved]
private NotificationOverlay notificationOverlay { get; set; }
- private SampleChannel shutter;
+ private Sample shutter;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, Storage storage, AudioManager audio)
diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
index abaae7b43c..b499b26f38 100644
--- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
+++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
@@ -22,8 +22,8 @@ namespace osu.Game.Graphics.UserInterface
private const int text_size = 17;
private const int transition_length = 80;
- private SampleChannel sampleClick;
- private SampleChannel sampleHover;
+ private Sample sampleClick;
+ private Sample sampleHover;
private TextContainer text;
diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
index 803facae04..c1963ce62d 100644
--- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
+++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Graphics.UserInterface
///
public class HoverClickSounds : HoverSounds
{
- private SampleChannel sampleClick;
+ private Sample sampleClick;
private readonly MouseButton[] buttons;
///
diff --git a/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs
new file mode 100644
index 0000000000..55f43cfe46
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osu.Game.Configuration;
+
+namespace osu.Game.Graphics.UserInterface
+{
+ ///
+ /// Handles debouncing hover sounds at a global level to ensure the effects are not overwhelming.
+ ///
+ public abstract class HoverSampleDebounceComponent : CompositeDrawable
+ {
+ ///
+ /// Length of debounce for hover sound playback, in milliseconds.
+ ///
+ public double HoverDebounceTime { get; } = 20;
+
+ private Bindable lastPlaybackTime;
+
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio, SessionStatics statics)
+ {
+ lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime);
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ // hover sounds shouldn't be played during scroll operations.
+ if (e.HasAnyButtonPressed)
+ return false;
+
+ bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime;
+
+ if (enoughTimePassedSinceLastPlayback)
+ {
+ PlayHoverSample();
+ lastPlaybackTime.Value = Time.Current;
+ }
+
+ return false;
+ }
+
+ public abstract void PlayHoverSample();
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs
index a1d06711db..00cf5798e7 100644
--- a/osu.Game/Graphics/UserInterface/HoverSounds.cs
+++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs
@@ -5,12 +5,10 @@ using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Input.Events;
using osu.Game.Configuration;
+using osu.Framework.Utils;
namespace osu.Game.Graphics.UserInterface
{
@@ -18,19 +16,12 @@ namespace osu.Game.Graphics.UserInterface
/// Adds hover sounds to a drawable.
/// Does not draw anything.
///
- public class HoverSounds : CompositeDrawable
+ public class HoverSounds : HoverSampleDebounceComponent
{
- private SampleChannel sampleHover;
-
- ///
- /// Length of debounce for hover sound playback, in milliseconds.
- ///
- public double HoverDebounceTime { get; } = 20;
+ private Sample sampleHover;
protected readonly HoverSampleSet SampleSet;
- private Bindable lastPlaybackTime;
-
public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal)
{
SampleSet = sampleSet;
@@ -40,22 +31,13 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio, SessionStatics statics)
{
- lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime);
-
sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}");
}
- protected override bool OnHover(HoverEvent e)
+ public override void PlayHoverSample()
{
- bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime;
-
- if (enoughTimePassedSinceLastPlayback)
- {
- sampleHover?.Play();
- lastPlaybackTime.Value = Time.Current;
- }
-
- return base.OnHover(e);
+ sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08);
+ sampleHover.Play();
}
}
@@ -68,6 +50,9 @@ namespace osu.Game.Graphics.UserInterface
Normal,
[Description("-softer")]
- Soft
+ Soft,
+
+ [Description("-toolbar")]
+ Toolbar
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
index f6effa0834..5f2d884cd7 100644
--- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
@@ -46,8 +46,8 @@ namespace osu.Game.Graphics.UserInterface
protected readonly Nub Nub;
private readonly OsuTextFlowContainer labelText;
- private SampleChannel sampleChecked;
- private SampleChannel sampleUnchecked;
+ private Sample sampleChecked;
+ private Sample sampleUnchecked;
public OsuCheckbox(bool nubOnRight = true)
{
@@ -64,7 +64,7 @@ namespace osu.Game.Graphics.UserInterface
RelativeSizeAxes = Axes.X,
},
Nub = new Nub(),
- new HoverClickSounds()
+ new HoverSounds()
};
if (nubOnRight)
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index d0356e77c7..f58962f8e1 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Graphics.UserInterface
///
private const int max_decimal_digits = 5;
- private SampleChannel sample;
+ private Sample sample;
private double lastSampleTime;
private T lastSampleValue;
@@ -155,16 +155,15 @@ namespace osu.Game.Graphics.UserInterface
return;
lastSampleValue = value;
-
lastSampleTime = Clock.CurrentTime;
- sample.Frequency.Value = 1 + NormalizedValue * 0.2f;
+ var channel = sample.Play();
+
+ channel.Frequency.Value = 1 + NormalizedValue * 0.2f;
if (NormalizedValue == 0)
- sample.Frequency.Value -= 0.4f;
+ channel.Frequency.Value -= 0.4f;
else if (NormalizedValue == 1)
- sample.Frequency.Value += 0.4f;
-
- sample.Play();
+ channel.Frequency.Value += 0.4f;
}
private void updateTooltipText(T value)
diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
index 1ec4dfc91a..75af9efc38 100644
--- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
@@ -23,11 +23,11 @@ namespace osu.Game.Graphics.UserInterface
{
public class OsuTextBox : BasicTextBox
{
- private readonly SampleChannel[] textAddedSamples = new SampleChannel[4];
- private SampleChannel capsTextAddedSample;
- private SampleChannel textRemovedSample;
- private SampleChannel textCommittedSample;
- private SampleChannel caretMovedSample;
+ private readonly Sample[] textAddedSamples = new Sample[4];
+ private Sample capsTextAddedSample;
+ private Sample textRemovedSample;
+ private Sample textCommittedSample;
+ private Sample caretMovedSample;
///
/// Whether to allow playing a different samples based on the type of character.
diff --git a/osu.Game/Graphics/UserInterface/ProgressBar.cs b/osu.Game/Graphics/UserInterface/ProgressBar.cs
index d271cd121c..50367e600e 100644
--- a/osu.Game/Graphics/UserInterface/ProgressBar.cs
+++ b/osu.Game/Graphics/UserInterface/ProgressBar.cs
@@ -40,8 +40,19 @@ namespace osu.Game.Graphics.UserInterface
set => CurrentNumber.Value = value;
}
- public ProgressBar()
+ private readonly bool allowSeek;
+
+ public override bool HandlePositionalInput => allowSeek;
+ public override bool HandleNonPositionalInput => allowSeek;
+
+ ///
+ /// Construct a new progress bar.
+ ///
+ /// Whether the user should be allowed to click/drag to adjust the value.
+ public ProgressBar(bool allowSeek)
{
+ this.allowSeek = allowSeek;
+
CurrentNumber.MinValue = 0;
CurrentNumber.MaxValue = 1;
RelativeSizeAxes = Axes.X;
diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs
new file mode 100644
index 0000000000..d4b0d300ff
--- /dev/null
+++ b/osu.Game/IO/StableStorage.cs
@@ -0,0 +1,62 @@
+// 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.IO;
+using System.Linq;
+using osu.Framework.Platform;
+
+namespace osu.Game.IO
+{
+ ///
+ /// A storage pointing to an osu-stable installation.
+ /// Provides methods for handling installations with a custom Song folder location.
+ ///
+ public class StableStorage : DesktopStorage
+ {
+ private const string stable_default_songs_path = "Songs";
+
+ private readonly DesktopGameHost host;
+ private readonly Lazy songsPath;
+
+ public StableStorage(string path, DesktopGameHost host)
+ : base(path, host)
+ {
+ this.host = host;
+
+ songsPath = new Lazy(locateSongsDirectory);
+ }
+
+ ///
+ /// Returns a pointing to the osu-stable Songs directory.
+ ///
+ public Storage GetSongStorage() => new DesktopStorage(songsPath.Value, host);
+
+ private string locateSongsDirectory()
+ {
+ var configFile = GetFiles(".", $"osu!.{Environment.UserName}.cfg").SingleOrDefault();
+
+ if (configFile != null)
+ {
+ using (var stream = GetStream(configFile))
+ using (var textReader = new StreamReader(stream))
+ {
+ string line;
+
+ while ((line = textReader.ReadLine()) != null)
+ {
+ if (!line.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase)) continue;
+
+ var customDirectory = line.Split('=').LastOrDefault()?.Trim();
+ if (customDirectory != null && Path.IsPathFullyQualified(customDirectory))
+ return customDirectory;
+
+ break;
+ }
+ }
+ }
+
+ return GetFullPath(stable_default_songs_path);
+ }
+ }
+}
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 2aaea22155..8ffa0221c8 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Online.API
{
private readonly OsuConfigManager config;
+ private readonly string versionHash;
+
private readonly OAuth authentication;
private readonly Queue queue = new Queue();
@@ -56,9 +58,10 @@ namespace osu.Game.Online.API
private readonly Logger log;
- public APIAccess(OsuConfigManager config, EndpointConfiguration endpointConfiguration)
+ public APIAccess(OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash)
{
this.config = config;
+ this.versionHash = versionHash;
APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl;
@@ -243,6 +246,8 @@ namespace osu.Game.Online.API
this.password = password;
}
+ public IHubClientConnector GetHubConnector(string clientName, string endpoint) => new HubClientConnector(clientName, endpoint, this, versionHash);
+
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
Debug.Assert(State.Value == APIState.Offline);
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 3e996ac97f..943b52db88 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -83,6 +83,8 @@ namespace osu.Game.Online.API
state.Value = APIState.Offline;
}
+ public IHubClientConnector GetHubConnector(string clientName, string endpoint) => null;
+
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
Thread.Sleep(200);
diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs
index 1951dfaf40..3a77b9cfee 100644
--- a/osu.Game/Online/API/IAPIProvider.cs
+++ b/osu.Game/Online/API/IAPIProvider.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.
+#nullable enable
+
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Game.Users;
@@ -95,6 +97,13 @@ namespace osu.Game.Online.API
///
void Logout();
+ ///
+ /// Constructs a new . May be null if not supported.
+ ///
+ /// The name of the client this connector connects for, used for logging.
+ /// The endpoint to the hub.
+ IHubClientConnector? GetHubConnector(string clientName, string endpoint);
+
///
/// Create a new user account. This is a blocking operation.
///
@@ -102,6 +111,6 @@ namespace osu.Game.Online.API
/// The username to create the account with.
/// The password to create the account with.
/// Any errors encoutnered during account creation.
- RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password);
+ RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password);
}
}
diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
index 99e87677fa..dd854acc32 100644
--- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
+++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
@@ -3,6 +3,7 @@
using System.Buffers;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Text;
using MessagePack;
using MessagePack.Formatters;
@@ -41,6 +42,13 @@ namespace osu.Game.Online.API
primitiveFormatter.Serialize(ref writer, b.Value, options);
break;
+ case IBindable u:
+ // A mod with unknown (e.g. enum) generic type.
+ var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value));
+ Debug.Assert(valueMethod != null);
+ primitiveFormatter.Serialize(ref writer, valueMethod.GetValue(u), options);
+ break;
+
default:
// fall back for non-bindable cases.
primitiveFormatter.Serialize(ref writer, kvp.Value, options);
diff --git a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
index bcc8721400..172fa3a583 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Online.API.Requests.Responses
public double? PP { get; set; }
[JsonProperty(@"room_id")]
- public int RoomID { get; set; }
+ public long RoomID { get; set; }
[JsonProperty("total_score")]
public long TotalScore { get; set; }
diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs
index d2a117876d..b80720a0aa 100644
--- a/osu.Game/Online/Chat/MessageFormatter.cs
+++ b/osu.Game/Online/Chat/MessageFormatter.cs
@@ -49,6 +49,18 @@ namespace osu.Game.Online.Chat
// Unicode emojis
private static readonly Regex emoji_regex = new Regex(@"(\uD83D[\uDC00-\uDE4F])");
+ ///
+ /// The root URL for the website, used for chat link matching.
+ ///
+ public static string WebsiteRootUrl
+ {
+ set => websiteRootUrl = value
+ .Trim('/') // trim potential trailing slash/
+ .Split('/').Last(); // only keep domain name, ignoring protocol.
+ }
+
+ private static string websiteRootUrl = "osu.ppy.sh";
+
private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null, char[] escapeChars = null)
{
int captureOffset = 0;
@@ -119,22 +131,42 @@ namespace osu.Game.Online.Chat
case "http":
case "https":
// length > 3 since all these links need another argument to work
- if (args.Length > 3 && args[1] == "osu.ppy.sh")
+ if (args.Length > 3 && args[1].EndsWith(websiteRootUrl, StringComparison.OrdinalIgnoreCase))
{
+ var mainArg = args[3];
+
switch (args[2])
{
+ // old site only
case "b":
case "beatmaps":
- return new LinkDetails(LinkAction.OpenBeatmap, args[3]);
+ {
+ string trimmed = mainArg.Split('?').First();
+ if (int.TryParse(trimmed, out var id))
+ return new LinkDetails(LinkAction.OpenBeatmap, id.ToString());
+
+ break;
+ }
case "s":
case "beatmapsets":
case "d":
- return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]);
+ {
+ if (args.Length > 4 && int.TryParse(args[4], out var id))
+ // https://osu.ppy.sh/beatmapsets/1154158#osu/2768184
+ return new LinkDetails(LinkAction.OpenBeatmap, id.ToString());
+
+ // https://osu.ppy.sh/beatmapsets/1154158#whatever
+ string trimmed = mainArg.Split('#').First();
+ if (int.TryParse(trimmed, out id))
+ return new LinkDetails(LinkAction.OpenBeatmapSet, id.ToString());
+
+ break;
+ }
case "u":
case "users":
- return new LinkDetails(LinkAction.OpenUserProfile, args[3]);
+ return new LinkDetails(LinkAction.OpenUserProfile, mainArg);
}
}
@@ -183,10 +215,9 @@ namespace osu.Game.Online.Chat
case "osump":
return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]);
-
- default:
- return new LinkDetails(LinkAction.External, null);
}
+
+ return new LinkDetails(LinkAction.External, null);
}
private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3)
@@ -259,8 +290,9 @@ namespace osu.Game.Online.Chat
public class LinkDetails
{
- public LinkAction Action;
- public string Argument;
+ public readonly LinkAction Action;
+
+ public readonly string Argument;
public LinkDetails(LinkAction action, string argument)
{
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
new file mode 100644
index 0000000000..fdb21c5000
--- /dev/null
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -0,0 +1,208 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR.Client;
+using Microsoft.Extensions.DependencyInjection;
+using Newtonsoft.Json;
+using osu.Framework;
+using osu.Framework.Bindables;
+using osu.Framework.Logging;
+using osu.Game.Online.API;
+
+namespace osu.Game.Online
+{
+ public class HubClientConnector : IHubClientConnector
+ {
+ ///
+ /// Invoked whenever a new hub connection is built, to configure it before it's started.
+ ///
+ public Action? ConfigureConnection { get; set; }
+
+ private readonly string clientName;
+ private readonly string endpoint;
+ private readonly string versionHash;
+ private readonly IAPIProvider api;
+
+ ///
+ /// The current connection opened by this connector.
+ ///
+ public HubConnection? CurrentConnection { get; private set; }
+
+ ///
+ /// Whether this is connected to the hub, use to access the connection, if this is true.
+ ///
+ public IBindable IsConnected => isConnected;
+
+ private readonly Bindable isConnected = new Bindable();
+ private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
+ private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
+
+ private readonly IBindable apiState = new Bindable();
+
+ ///
+ /// Constructs a new .
+ ///
+ /// The name of the client this connector connects for, used for logging.
+ /// The endpoint to the hub.
+ /// An API provider used to react to connection state changes.
+ /// The hash representing the current game version, used for verification purposes.
+ public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash)
+ {
+ this.clientName = clientName;
+ this.endpoint = endpoint;
+ this.api = api;
+ this.versionHash = versionHash;
+
+ apiState.BindTo(api.State);
+ apiState.BindValueChanged(state =>
+ {
+ switch (state.NewValue)
+ {
+ case APIState.Failing:
+ case APIState.Offline:
+ Task.Run(() => disconnect(true));
+ break;
+
+ case APIState.Online:
+ Task.Run(connect);
+ break;
+ }
+ }, true);
+ }
+
+ private async Task connect()
+ {
+ cancelExistingConnect();
+
+ if (!await connectionLock.WaitAsync(10000))
+ throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
+
+ try
+ {
+ while (apiState.Value == APIState.Online)
+ {
+ // ensure any previous connection was disposed.
+ // this will also create a new cancellation token source.
+ await disconnect(false);
+
+ // this token will be valid for the scope of this connection.
+ // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
+ var cancellationToken = connectCancelSource.Token;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Logger.Log($"{clientName} connecting...", LoggingTarget.Network);
+
+ try
+ {
+ // importantly, rebuild the connection each attempt to get an updated access token.
+ CurrentConnection = buildConnection(cancellationToken);
+
+ await CurrentConnection.StartAsync(cancellationToken);
+
+ Logger.Log($"{clientName} connected!", LoggingTarget.Network);
+ isConnected.Value = true;
+ return;
+ }
+ catch (OperationCanceledException)
+ {
+ //connection process was cancelled.
+ throw;
+ }
+ catch (Exception e)
+ {
+ Logger.Log($"{clientName} connection error: {e}", LoggingTarget.Network);
+
+ // retry on any failure.
+ await Task.Delay(5000, cancellationToken);
+ }
+ }
+ }
+ finally
+ {
+ connectionLock.Release();
+ }
+ }
+
+ private HubConnection buildConnection(CancellationToken cancellationToken)
+ {
+ var builder = new HubConnectionBuilder()
+ .WithUrl(endpoint, options =>
+ {
+ options.Headers.Add("Authorization", $"Bearer {api.AccessToken}");
+ options.Headers.Add("OsuVersionHash", versionHash);
+ });
+
+ if (RuntimeInfo.SupportsJIT)
+ builder.AddMessagePackProtocol();
+ else
+ {
+ // eventually we will precompile resolvers for messagepack, but this isn't working currently
+ // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
+ builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
+ }
+
+ var newConnection = builder.Build();
+
+ ConfigureConnection?.Invoke(newConnection);
+
+ newConnection.Closed += ex => onConnectionClosed(ex, cancellationToken);
+ return newConnection;
+ }
+
+ private Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
+ {
+ isConnected.Value = false;
+
+ Logger.Log(ex != null ? $"{clientName} lost connection: {ex}" : $"{clientName} disconnected", LoggingTarget.Network);
+
+ // make sure a disconnect wasn't triggered (and this is still the active connection).
+ if (!cancellationToken.IsCancellationRequested)
+ Task.Run(connect, default);
+
+ return Task.CompletedTask;
+ }
+
+ private async Task disconnect(bool takeLock)
+ {
+ cancelExistingConnect();
+
+ if (takeLock)
+ {
+ if (!await connectionLock.WaitAsync(10000))
+ throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
+ }
+
+ try
+ {
+ if (CurrentConnection != null)
+ await CurrentConnection.DisposeAsync();
+ }
+ finally
+ {
+ CurrentConnection = null;
+ if (takeLock)
+ connectionLock.Release();
+ }
+ }
+
+ private void cancelExistingConnect()
+ {
+ connectCancelSource.Cancel();
+ connectCancelSource = new CancellationTokenSource();
+ }
+
+ public override string ToString() => $"Connector for {clientName} ({(IsConnected.Value ? "connected" : "not connected")}";
+
+ public void Dispose()
+ {
+ apiState.UnbindAll();
+ cancelExistingConnect();
+ }
+ }
+}
diff --git a/osu.Game/Online/IHubClientConnector.cs b/osu.Game/Online/IHubClientConnector.cs
new file mode 100644
index 0000000000..d2ceb1f030
--- /dev/null
+++ b/osu.Game/Online/IHubClientConnector.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+using Microsoft.AspNetCore.SignalR.Client;
+using osu.Framework.Bindables;
+using osu.Game.Online.API;
+
+namespace osu.Game.Online
+{
+ ///
+ /// A component that manages the life cycle of a connection to a SignalR Hub.
+ /// Should generally be retrieved from an .
+ ///
+ public interface IHubClientConnector : IDisposable
+ {
+ ///
+ /// The current connection opened by this connector.
+ ///
+ HubConnection? CurrentConnection { get; }
+
+ ///
+ /// Whether this is connected to the hub, use to access the connection, if this is true.
+ ///
+ IBindable IsConnected { get; }
+
+ ///
+ /// Invoked whenever a new hub connection is built, to configure it before it's started.
+ ///
+ public Action? ConfigureConnection { get; set; }
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 493518ac80..95d76f384f 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -3,17 +3,12 @@
#nullable enable
-using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
-using Microsoft.Extensions.DependencyInjection;
-using Newtonsoft.Json;
-using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@@ -21,106 +16,50 @@ namespace osu.Game.Online.Multiplayer
{
public class MultiplayerClient : StatefulMultiplayerClient
{
- public override IBindable IsConnected => isConnected;
-
- private readonly Bindable isConnected = new Bindable();
- private readonly IBindable apiState = new Bindable();
-
- private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
-
- [Resolved]
- private IAPIProvider api { get; set; } = null!;
-
- private HubConnection? connection;
-
- private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
-
private readonly string endpoint;
+ private IHubClientConnector? connector;
+
+ public override IBindable IsConnected { get; } = new BindableBool();
+
+ private HubConnection? connection => connector?.CurrentConnection;
+
public MultiplayerClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.MultiplayerEndpointUrl;
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(IAPIProvider api)
{
- apiState.BindTo(api.State);
- apiState.BindValueChanged(apiStateChanged, true);
- }
+ connector = api.GetHubConnector(nameof(MultiplayerClient), endpoint);
- private void apiStateChanged(ValueChangedEvent state)
- {
- switch (state.NewValue)
+ if (connector != null)
{
- case APIState.Failing:
- case APIState.Offline:
- Task.Run(() => disconnect(true));
- break;
-
- case APIState.Online:
- Task.Run(connect);
- break;
- }
- }
-
- private async Task connect()
- {
- cancelExistingConnect();
-
- if (!await connectionLock.WaitAsync(10000))
- throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
-
- try
- {
- while (api.State.Value == APIState.Online)
+ connector.ConfigureConnection = connection =>
{
- // ensure any previous connection was disposed.
- // this will also create a new cancellation token source.
- await disconnect(false);
+ // this is kind of SILLY
+ // https://github.com/dotnet/aspnetcore/issues/15198
+ connection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
+ connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
+ connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
+ connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
+ connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
+ connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
+ connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
+ connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
+ connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
+ connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
+ connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
+ };
- // this token will be valid for the scope of this connection.
- // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
- var cancellationToken = connectCancelSource.Token;
-
- cancellationToken.ThrowIfCancellationRequested();
-
- Logger.Log("Multiplayer client connecting...", LoggingTarget.Network);
-
- try
- {
- // importantly, rebuild the connection each attempt to get an updated access token.
- connection = createConnection(cancellationToken);
-
- await connection.StartAsync(cancellationToken);
-
- Logger.Log("Multiplayer client connected!", LoggingTarget.Network);
- isConnected.Value = true;
- return;
- }
- catch (OperationCanceledException)
- {
- //connection process was cancelled.
- throw;
- }
- catch (Exception e)
- {
- Logger.Log($"Multiplayer client connection error: {e}", LoggingTarget.Network);
-
- // retry on any failure.
- await Task.Delay(5000, cancellationToken);
- }
- }
- }
- finally
- {
- connectionLock.Release();
+ IsConnected.BindTo(connector.IsConnected);
}
}
protected override Task JoinRoom(long roomId)
{
- if (!isConnected.Value)
+ if (!IsConnected.Value)
return Task.FromCanceled(new CancellationToken(true));
return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoom), roomId);
@@ -128,7 +67,7 @@ namespace osu.Game.Online.Multiplayer
protected override Task LeaveRoomInternal()
{
- if (!isConnected.Value)
+ if (!IsConnected.Value)
return Task.FromCanceled(new CancellationToken(true));
return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom));
@@ -136,7 +75,7 @@ namespace osu.Game.Online.Multiplayer
public override Task TransferHost(int userId)
{
- if (!isConnected.Value)
+ if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId);
@@ -144,7 +83,7 @@ namespace osu.Game.Online.Multiplayer
public override Task ChangeSettings(MultiplayerRoomSettings settings)
{
- if (!isConnected.Value)
+ if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings);
@@ -152,7 +91,7 @@ namespace osu.Game.Online.Multiplayer
public override Task ChangeState(MultiplayerUserState newState)
{
- if (!isConnected.Value)
+ if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
@@ -160,7 +99,7 @@ namespace osu.Game.Online.Multiplayer
public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability)
{
- if (!isConnected.Value)
+ if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability);
@@ -168,7 +107,7 @@ namespace osu.Game.Online.Multiplayer
public override Task ChangeUserMods(IEnumerable newMods)
{
- if (!isConnected.Value)
+ if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserMods), newMods);
@@ -176,90 +115,16 @@ namespace osu.Game.Online.Multiplayer
public override Task StartMatch()
{
- if (!isConnected.Value)
+ if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
}
- private async Task disconnect(bool takeLock)
- {
- cancelExistingConnect();
-
- if (takeLock)
- {
- if (!await connectionLock.WaitAsync(10000))
- throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
- }
-
- try
- {
- if (connection != null)
- await connection.DisposeAsync();
- }
- finally
- {
- connection = null;
- if (takeLock)
- connectionLock.Release();
- }
- }
-
- private void cancelExistingConnect()
- {
- connectCancelSource.Cancel();
- connectCancelSource = new CancellationTokenSource();
- }
-
- private HubConnection createConnection(CancellationToken cancellationToken)
- {
- var builder = new HubConnectionBuilder()
- .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
-
- if (RuntimeInfo.SupportsJIT)
- builder.AddMessagePackProtocol();
- else
- {
- // eventually we will precompile resolvers for messagepack, but this isn't working currently
- // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
- builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
- }
-
- var newConnection = builder.Build();
-
- // this is kind of SILLY
- // https://github.com/dotnet/aspnetcore/issues/15198
- newConnection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
- newConnection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
- newConnection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
- newConnection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
- newConnection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
- newConnection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
- newConnection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
- newConnection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
- newConnection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
- newConnection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
-
- newConnection.Closed += ex =>
- {
- isConnected.Value = false;
-
- Logger.Log(ex != null ? $"Multiplayer client lost connection: {ex}" : "Multiplayer client disconnected", LoggingTarget.Network);
-
- // make sure a disconnect wasn't triggered (and this is still the active connection).
- if (!cancellationToken.IsCancellationRequested)
- Task.Run(connect, default);
-
- return Task.CompletedTask;
- };
- return newConnection;
- }
-
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
-
- cancelExistingConnect();
+ connector?.Dispose();
}
}
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
index 4fb9d724b5..7d6c76bc2f 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
@@ -36,18 +36,23 @@ namespace osu.Game.Online.Multiplayer
[Key(5)]
public IEnumerable AllowedMods { get; set; } = Enumerable.Empty();
+ [Key(6)]
+ public long PlaylistItemId { get; set; }
+
public bool Equals(MultiplayerRoomSettings other)
=> BeatmapID == other.BeatmapID
&& BeatmapChecksum == other.BeatmapChecksum
&& RequiredMods.SequenceEqual(other.RequiredMods)
&& AllowedMods.SequenceEqual(other.AllowedMods)
&& RulesetID == other.RulesetID
- && Name.Equals(other.Name, StringComparison.Ordinal);
+ && Name.Equals(other.Name, StringComparison.Ordinal)
+ && PlaylistItemId == other.PlaylistItemId;
public override string ToString() => $"Name:{Name}"
+ $" Beatmap:{BeatmapID} ({BeatmapChecksum})"
+ $" RequiredMods:{string.Join(',', RequiredMods)}"
+ $" AllowedMods:{string.Join(',', AllowedMods)}"
- + $" Ruleset:{RulesetID}";
+ + $" Ruleset:{RulesetID}"
+ + $" Item:{PlaylistItemId}";
}
}
diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
index f454fe619b..bfd505fb19 100644
--- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
@@ -66,6 +66,8 @@ namespace osu.Game.Online.Multiplayer
///
public readonly BindableList CurrentMatchPlayingUserIds = new BindableList();
+ public readonly Bindable CurrentMatchPlayingItem = new Bindable();
+
///
/// The corresponding to the local player, if available.
///
@@ -92,12 +94,14 @@ namespace osu.Game.Online.Multiplayer
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
+ // Only exists for compatibility with old osu-server-spectator build.
+ // Todo: Can be removed on 2021/02/26.
+ private long defaultPlaylistItemId;
+
private Room? apiRoom;
- // Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise.
- private int playlistItemId;
-
- protected StatefulMultiplayerClient()
+ [BackgroundDependencyLoader]
+ private void load()
{
IsConnected.BindValueChanged(connected =>
{
@@ -141,7 +145,7 @@ namespace osu.Game.Online.Multiplayer
{
Room = joinedRoom;
apiRoom = room;
- playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0;
+ defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0;
}, cancellationSource.Token);
// Update room settings.
@@ -217,7 +221,7 @@ namespace osu.Game.Online.Multiplayer
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods,
- AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods
+ AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods,
});
}
@@ -505,14 +509,13 @@ namespace osu.Game.Online.Multiplayer
Room.Settings = settings;
apiRoom.Name.Value = Room.Settings.Name;
- // The playlist update is delayed until an online beatmap lookup (below) succeeds.
- // In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here.
- apiRoom.Playlist.Clear();
+ // The current item update is delayed until an online beatmap lookup (below) succeeds.
+ // In-order for the client to not display an outdated beatmap, the current item is forcefully cleared here.
+ CurrentMatchPlayingItem.Value = null;
RoomUpdated?.Invoke();
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
-
req.Success += res =>
{
if (cancellationToken.IsCancellationRequested)
@@ -539,18 +542,30 @@ namespace osu.Game.Online.Multiplayer
var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset));
var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset));
- PlaylistItem playlistItem = new PlaylistItem
+ // Try to retrieve the existing playlist item from the API room.
+ var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId);
+
+ if (playlistItem != null)
+ updateItem(playlistItem);
+ else
{
- ID = playlistItemId,
- Beatmap = { Value = beatmap },
- Ruleset = { Value = ruleset.RulesetInfo },
- };
+ // An existing playlist item does not exist, so append a new one.
+ updateItem(playlistItem = new PlaylistItem());
+ apiRoom.Playlist.Add(playlistItem);
+ }
- playlistItem.RequiredMods.AddRange(mods);
- playlistItem.AllowedMods.AddRange(allowedMods);
+ CurrentMatchPlayingItem.Value = playlistItem;
- apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity.
- apiRoom.Playlist.Add(playlistItem);
+ void updateItem(PlaylistItem item)
+ {
+ item.ID = settings.PlaylistItemId == 0 ? defaultPlaylistItemId : settings.PlaylistItemId;
+ item.Beatmap.Value = beatmap;
+ item.Ruleset.Value = ruleset.RulesetInfo;
+ item.RequiredMods.Clear();
+ item.RequiredMods.AddRange(mods);
+ item.AllowedMods.Clear();
+ item.AllowedMods.AddRange(allowedMods);
+ }
}
///
diff --git a/osu.Game/Online/OnlineViewContainer.cs b/osu.Game/Online/OnlineViewContainer.cs
index c9fb70f0cc..8868f90524 100644
--- a/osu.Game/Online/OnlineViewContainer.cs
+++ b/osu.Game/Online/OnlineViewContainer.cs
@@ -15,7 +15,7 @@ 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
+ public class OnlineViewContainer : Container
{
protected LoadingSpinner LoadingSpinner { get; private set; }
@@ -30,7 +30,7 @@ namespace osu.Game.Online
[Resolved]
protected IAPIProvider API { get; private set; }
- protected OnlineViewContainer(string placeholderMessage)
+ public OnlineViewContainer(string placeholderMessage)
{
this.placeholderMessage = placeholderMessage;
}
diff --git a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs
index afd0dadc7e..d4303e77df 100644
--- a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs
+++ b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs
@@ -9,11 +9,11 @@ namespace osu.Game.Online.Rooms
{
public class CreateRoomScoreRequest : APIRequest
{
- private readonly int roomId;
- private readonly int playlistItemId;
+ private readonly long roomId;
+ private readonly long playlistItemId;
private readonly string versionHash;
- public CreateRoomScoreRequest(int roomId, int playlistItemId, string versionHash)
+ public CreateRoomScoreRequest(long roomId, long playlistItemId, string versionHash)
{
this.roomId = roomId;
this.playlistItemId = playlistItemId;
diff --git a/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs
index 15f1221a00..67e2a2b27f 100644
--- a/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs
+++ b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs
@@ -7,9 +7,9 @@ namespace osu.Game.Online.Rooms
{
public class GetRoomLeaderboardRequest : APIRequest
{
- private readonly int roomId;
+ private readonly long roomId;
- public GetRoomLeaderboardRequest(int roomId)
+ public GetRoomLeaderboardRequest(long roomId)
{
this.roomId = roomId;
}
diff --git a/osu.Game/Online/Rooms/GetRoomRequest.cs b/osu.Game/Online/Rooms/GetRoomRequest.cs
index ce117075c7..853873901e 100644
--- a/osu.Game/Online/Rooms/GetRoomRequest.cs
+++ b/osu.Game/Online/Rooms/GetRoomRequest.cs
@@ -7,9 +7,9 @@ namespace osu.Game.Online.Rooms
{
public class GetRoomRequest : APIRequest
{
- public readonly int RoomId;
+ public readonly long RoomId;
- public GetRoomRequest(int roomId)
+ public GetRoomRequest(long roomId)
{
RoomId = roomId;
}
diff --git a/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs
index 43f80a2dc4..abce2093e3 100644
--- a/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs
+++ b/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs
@@ -15,8 +15,8 @@ namespace osu.Game.Online.Rooms
///
public class IndexPlaylistScoresRequest : APIRequest
{
- public readonly int RoomId;
- public readonly int PlaylistItemId;
+ public readonly long RoomId;
+ public readonly long PlaylistItemId;
[CanBeNull]
public readonly Cursor Cursor;
@@ -24,13 +24,13 @@ namespace osu.Game.Online.Rooms
[CanBeNull]
public readonly IndexScoresParams IndexParams;
- public IndexPlaylistScoresRequest(int roomId, int playlistItemId)
+ public IndexPlaylistScoresRequest(long roomId, long playlistItemId)
{
RoomId = roomId;
PlaylistItemId = playlistItemId;
}
- public IndexPlaylistScoresRequest(int roomId, int playlistItemId, [NotNull] Cursor cursor, [NotNull] IndexScoresParams indexParams)
+ public IndexPlaylistScoresRequest(long roomId, long playlistItemId, [NotNull] Cursor cursor, [NotNull] IndexScoresParams indexParams)
: this(roomId, playlistItemId)
{
Cursor = cursor;
diff --git a/osu.Game/Online/Rooms/ItemAttemptsCount.cs b/osu.Game/Online/Rooms/ItemAttemptsCount.cs
new file mode 100644
index 0000000000..298603d778
--- /dev/null
+++ b/osu.Game/Online/Rooms/ItemAttemptsCount.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.Rooms
+{
+ ///
+ /// Represents attempts on a specific playlist item.
+ ///
+ public class ItemAttemptsCount
+ {
+ [JsonProperty("id")]
+ public int PlaylistItemID { get; set; }
+
+ [JsonProperty("attempts")]
+ public int Attempts { get; set; }
+ }
+}
diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs
index 677a3d3026..30c1d2f826 100644
--- a/osu.Game/Online/Rooms/MultiplayerScore.cs
+++ b/osu.Game/Online/Rooms/MultiplayerScore.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Online.Rooms
public class MultiplayerScore
{
[JsonProperty("id")]
- public int ID { get; set; }
+ public long ID { get; set; }
[JsonProperty("user")]
public User User { get; set; }
diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs
index ad4b3c5151..d6f4c45a75 100644
--- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs
+++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Logging;
+using osu.Framework.Threading;
using osu.Game.Beatmaps;
namespace osu.Game.Online.Rooms
@@ -24,19 +25,33 @@ namespace osu.Game.Online.Rooms
///
public IBindable Availability => availability;
- private readonly Bindable availability = new Bindable();
+ private readonly Bindable availability = new Bindable(BeatmapAvailability.LocallyAvailable());
- public OnlinePlayBeatmapAvailablilityTracker()
- {
- State.BindValueChanged(_ => updateAvailability());
- Progress.BindValueChanged(_ => updateAvailability(), true);
- }
+ private ScheduledDelegate progressUpdate;
protected override void LoadComplete()
{
base.LoadComplete();
- SelectedItem.BindValueChanged(item => Model.Value = item.NewValue?.Beatmap.Value.BeatmapSet, true);
+ SelectedItem.BindValueChanged(item =>
+ {
+ // the underlying playlist is regularly cleared for maintenance purposes (things which probably need to be fixed eventually).
+ // to avoid exposing a state change when there may actually be none, ignore all nulls for now.
+ if (item.NewValue == null)
+ return;
+
+ Model.Value = item.NewValue.Beatmap.Value.BeatmapSet;
+ }, true);
+
+ Progress.BindValueChanged(_ =>
+ {
+ // incoming progress changes are going to be at a very high rate.
+ // we don't want to flood the network with this, so rate limit how often we send progress updates.
+ if (progressUpdate?.Completed != false)
+ progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
+ });
+
+ State.BindValueChanged(_ => updateAvailability(), true);
}
protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet)
diff --git a/osu.Game/Online/Rooms/PlaylistAggregateScore.cs b/osu.Game/Online/Rooms/PlaylistAggregateScore.cs
new file mode 100644
index 0000000000..61e0951cd5
--- /dev/null
+++ b/osu.Game/Online/Rooms/PlaylistAggregateScore.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.Rooms
+{
+ ///
+ /// Represents aggregated score for the local user for a playlist.
+ ///
+ public class PlaylistAggregateScore
+ {
+ [JsonProperty("playlist_item_attempts")]
+ public ItemAttemptsCount[] PlaylistItemAttempts { get; set; }
+ }
+}
diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs
index ada2140ca6..1d409d4b56 100644
--- a/osu.Game/Online/Rooms/PlaylistItem.cs
+++ b/osu.Game/Online/Rooms/PlaylistItem.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Online.Rooms
public class PlaylistItem : IEquatable
{
[JsonProperty("id")]
- public int ID { get; set; }
+ public long ID { get; set; }
[JsonProperty("beatmap_id")]
public int BeatmapID { get; set; }
@@ -23,6 +23,12 @@ namespace osu.Game.Online.Rooms
[JsonProperty("ruleset_id")]
public int RulesetID { get; set; }
+ ///
+ /// Whether this is still a valid selection for the .
+ ///
+ [JsonProperty("expired")]
+ public bool Expired { get; set; }
+
[JsonIgnore]
public readonly Bindable Beatmap = new Bindable();
diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs
index 763ba25d52..b28680ffef 100644
--- a/osu.Game/Online/Rooms/Room.cs
+++ b/osu.Game/Online/Rooms/Room.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Online.Rooms
{
[Cached]
[JsonProperty("id")]
- public readonly Bindable RoomID = new Bindable();
+ public readonly Bindable RoomID = new Bindable();
[Cached]
[JsonProperty("name")]
@@ -72,6 +72,10 @@ namespace osu.Game.Online.Rooms
[JsonIgnore]
public readonly Bindable MaxParticipants = new Bindable();
+ [Cached]
+ [JsonProperty("current_user_score")]
+ public readonly Bindable UserScore = new Bindable();
+
[Cached]
[JsonProperty("recent_participants")]
public readonly BindableList RecentParticipants = new BindableList();
@@ -144,10 +148,17 @@ namespace osu.Game.Online.Rooms
MaxParticipants.Value = other.MaxParticipants.Value;
ParticipantCount.Value = other.ParticipantCount.Value;
EndDate.Value = other.EndDate.Value;
+ UserScore.Value = other.UserScore.Value;
if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value)
Status.Value = new RoomStatusEnded();
+ // Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended,
+ // and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room.
+ // More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room.
+ if (!(Status.Value is RoomStatusEnded))
+ other.Playlist.RemoveAll(i => i.Expired);
+
if (!Playlist.SequenceEqual(other.Playlist))
{
Playlist.Clear();
diff --git a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs
index 3f728a5417..ba3e3c6349 100644
--- a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs
+++ b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs
@@ -7,11 +7,11 @@ namespace osu.Game.Online.Rooms
{
public class ShowPlaylistUserScoreRequest : APIRequest
{
- private readonly int roomId;
- private readonly int playlistItemId;
+ private readonly long roomId;
+ private readonly long playlistItemId;
private readonly long userId;
- public ShowPlaylistUserScoreRequest(int roomId, int playlistItemId, long userId)
+ public ShowPlaylistUserScoreRequest(long roomId, long playlistItemId, long userId)
{
this.roomId = roomId;
this.playlistItemId = playlistItemId;
diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
index 5a78b9fabd..9e432fa99e 100644
--- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
+++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
@@ -11,12 +11,12 @@ namespace osu.Game.Online.Rooms
{
public class SubmitRoomScoreRequest : APIRequest
{
- private readonly int scoreId;
- private readonly int roomId;
- private readonly int playlistItemId;
+ private readonly long scoreId;
+ private readonly long roomId;
+ private readonly long playlistItemId;
private readonly ScoreInfo scoreInfo;
- public SubmitRoomScoreRequest(int scoreId, int roomId, int playlistItemId, ScoreInfo scoreInfo)
+ public SubmitRoomScoreRequest(long scoreId, long roomId, long playlistItemId, ScoreInfo scoreInfo)
{
this.scoreId = scoreId;
this.roomId = roomId;
diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs
index b95e3f1297..3a586874fe 100644
--- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs
@@ -8,13 +8,9 @@ using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.SignalR.Client;
-using Microsoft.Extensions.DependencyInjection;
-using Newtonsoft.Json;
-using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Replays.Legacy;
@@ -34,7 +30,14 @@ namespace osu.Game.Online.Spectator
///
public const double TIME_BETWEEN_SENDS = 200;
- private HubConnection connection;
+ private readonly string endpoint;
+
+ [CanBeNull]
+ private IHubClientConnector connector;
+
+ private readonly IBindable isConnected = new BindableBool();
+
+ private HubConnection connection => connector?.CurrentConnection;
private readonly List watchingUsers = new List();
@@ -44,13 +47,6 @@ namespace osu.Game.Online.Spectator
private readonly BindableList playingUsers = new BindableList();
- private readonly IBindable apiState = new Bindable();
-
- private bool isConnected;
-
- [Resolved]
- private IAPIProvider api { get; set; }
-
[CanBeNull]
private IBeatmap currentBeatmap;
@@ -82,85 +78,32 @@ namespace osu.Game.Online.Spectator
///
public event Action OnUserFinishedPlaying;
- private readonly string endpoint;
-
public SpectatorStreamingClient(EndpointConfiguration endpoints)
{
endpoint = endpoints.SpectatorEndpointUrl;
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(IAPIProvider api)
{
- apiState.BindTo(api.State);
- apiState.BindValueChanged(apiStateChanged, true);
- }
+ connector = api.GetHubConnector(nameof(SpectatorStreamingClient), endpoint);
- private void apiStateChanged(ValueChangedEvent state)
- {
- switch (state.NewValue)
+ if (connector != null)
{
- case APIState.Failing:
- case APIState.Offline:
- connection?.StopAsync();
- connection = null;
- break;
-
- case APIState.Online:
- Task.Run(Connect);
- break;
- }
- }
-
- protected virtual async Task Connect()
- {
- if (connection != null)
- return;
-
- var builder = new HubConnectionBuilder()
- .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); });
-
- if (RuntimeInfo.SupportsJIT)
- builder.AddMessagePackProtocol();
- else
- {
- // eventually we will precompile resolvers for messagepack, but this isn't working currently
- // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308.
- builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
- }
-
- connection = builder.Build();
- // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198)
- connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
- connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
- connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
-
- connection.Closed += async ex =>
- {
- isConnected = false;
- playingUsers.Clear();
-
- if (ex != null)
+ connector.ConfigureConnection = connection =>
{
- Logger.Log($"Spectator client lost connection: {ex}", LoggingTarget.Network);
- await tryUntilConnected();
- }
- };
+ // until strong typed client support is added, each method must be manually bound
+ // (see https://github.com/dotnet/aspnetcore/issues/15198)
+ connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying);
+ connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
+ connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
+ };
- await tryUntilConnected();
-
- async Task tryUntilConnected()
- {
- Logger.Log("Spectator client connecting...", LoggingTarget.Network);
-
- while (api.State.Value == APIState.Online)
+ isConnected.BindTo(connector.IsConnected);
+ isConnected.BindValueChanged(connected =>
{
- try
+ if (connected.NewValue)
{
- // reconnect on any failure
- await connection.StartAsync();
- Logger.Log("Spectator client connected!", LoggingTarget.Network);
-
// get all the users that were previously being watched
int[] users;
@@ -170,25 +113,19 @@ namespace osu.Game.Online.Spectator
watchingUsers.Clear();
}
- // success
- isConnected = true;
-
- // resubscribe to watched users
+ // resubscribe to watched users.
foreach (var userId in users)
WatchUser(userId);
// re-send state in case it wasn't received
if (isPlaying)
beginPlaying();
-
- break;
}
- catch (Exception e)
+ else
{
- Logger.Log($"Spectator client connection error: {e}", LoggingTarget.Network);
- await Task.Delay(5000);
+ playingUsers.Clear();
}
- }
+ }, true);
}
}
@@ -240,14 +177,14 @@ namespace osu.Game.Online.Spectator
{
Debug.Assert(isPlaying);
- if (!isConnected) return;
+ if (!isConnected.Value) return;
connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState);
}
public void SendFrames(FrameDataBundle data)
{
- if (!isConnected) return;
+ if (!isConnected.Value) return;
lastSend = connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data);
}
@@ -257,7 +194,7 @@ namespace osu.Game.Online.Spectator
isPlaying = false;
currentBeatmap = null;
- if (!isConnected) return;
+ if (!isConnected.Value) return;
connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState);
}
@@ -271,7 +208,7 @@ namespace osu.Game.Online.Spectator
watchingUsers.Add(userId);
- if (!isConnected)
+ if (!isConnected.Value)
return;
}
@@ -284,7 +221,7 @@ namespace osu.Game.Online.Spectator
{
watchingUsers.Remove(userId);
- if (!isConnected)
+ if (!isConnected.Value)
return;
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 1a1f7bd233..771bcd2310 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -28,7 +28,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
-using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Collections;
@@ -52,6 +51,7 @@ using osu.Game.Updater;
using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel;
using osu.Game.Database;
+using osu.Game.IO;
namespace osu.Game
{
@@ -88,7 +88,7 @@ namespace osu.Game
protected SentryLogger SentryLogger;
- public virtual Storage GetStorageForStableInstall() => null;
+ public virtual StableStorage GetStorageForStableInstall() => null;
public float ToolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0);
@@ -383,7 +383,7 @@ namespace osu.Game
Ruleset.Value = selection.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection);
- }, validScreens: new[] { typeof(PlaySongSelect) });
+ }, validScreens: new[] { typeof(SongSelect) });
}
///
@@ -778,7 +778,7 @@ namespace osu.Game
if (recentLogCount < short_term_display_limit)
{
- Schedule(() => notifications.Post(new SimpleNotification
+ Schedule(() => notifications.Post(new SimpleErrorNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 20d88d33f2..3d24f245f9 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -31,6 +31,7 @@ using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.IO;
using osu.Game.Online;
+using osu.Game.Online.Chat;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
@@ -98,7 +99,14 @@ namespace osu.Game
[Cached(typeof(IBindable))]
protected readonly Bindable Ruleset = new Bindable();
- // todo: move this to SongSelect once Screen has the ability to unsuspend.
+ ///
+ /// The current mod selection for the local user.
+ ///
+ ///
+ /// If a mod select overlay is present, mod instances set to this value are not guaranteed to remain as the provided instance and will be overwritten by a copy.
+ /// In such a case, changes to settings of a mod will *not* propagate after a mod is added to this collection.
+ /// As such, all settings should be finalised before adding a mod to this collection.
+ ///
[Cached]
[Cached(typeof(IBindable>))]
protected readonly Bindable> SelectedMods = new Bindable>(Array.Empty());
@@ -148,6 +156,13 @@ namespace osu.Game
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
+ ///
+ /// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects.
+ ///
+ internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.5;
+
+ private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(GLOBAL_TRACK_VOLUME_ADJUST);
+
[BackgroundDependencyLoader]
private void load()
{
@@ -216,7 +231,9 @@ namespace osu.Game
EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
- dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints));
+ MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
+
+ dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints, VersionHash));
dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient(endpoints));
dependencies.CacheAs(multiplayerClient = new MultiplayerClient(endpoints));
@@ -271,9 +288,10 @@ namespace osu.Game
RegisterImportHandler(ScoreManager);
RegisterImportHandler(SkinManager);
- // tracks play so loud our samples can't keep up.
- // this adds a global reduction of track volume for the time being.
- Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8));
+ // drop track volume game-wide to leave some head-room for UI effects / samples.
+ // this means that for the time being, gameplay sample playback is louder relative to the audio track, compared to stable.
+ // we may want to revisit this if users notice or complain about the difference (consider this a bit of a trial).
+ Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust);
Beatmap = new NonNullableBindable(defaultBeatmap);
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs
index 6a2f2e4569..ca94078401 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
public DownloadProgressBar(BeatmapSetInfo beatmapSet)
: base(beatmapSet)
{
- AddInternal(progressBar = new InteractionDisabledProgressBar
+ AddInternal(progressBar = new ProgressBar(false)
{
Height = 0,
Alpha = 0,
@@ -64,11 +64,5 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
}
}, true);
}
-
- private class InteractionDisabledProgressBar : ProgressBar
- {
- public override bool HandlePositionalInput => false;
- public override bool HandleNonPositionalInput => false;
- }
}
}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 698984b306..5df7a4650e 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -15,98 +15,82 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
-using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.BeatmapListing.Panels;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Overlays
{
- public class BeatmapListingOverlay : FullscreenOverlay
+ public class BeatmapListingOverlay : OnlineOverlay
{
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
private Drawable currentContent;
- private LoadingLayer loadingLayer;
private Container panelTarget;
private FillFlowContainer foundContent;
private NotFoundDrawable notFoundContent;
-
- private OverlayScrollContainer resultScrollContainer;
+ private BeatmapListingFilterControl filterControl;
public BeatmapListingOverlay()
- : base(OverlayColourScheme.Blue, new BeatmapListingHeader())
+ : base(OverlayColourScheme.Blue)
{
}
- private BeatmapListingFilterControl filterControl;
-
[BackgroundDependencyLoader]
private void load()
{
- Children = new Drawable[]
+ Child = new FillFlowContainer
{
- new Box
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background6
- },
- resultScrollContainer = new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new ReverseChildIDFillFlowContainer
+ filterControl = new BeatmapListingFilterControl
+ {
+ TypingStarted = onTypingStarted,
+ SearchStarted = onSearchStarted,
+ SearchFinished = onSearchFinished,
+ },
+ new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- Header,
- filterControl = new BeatmapListingFilterControl
+ new Box
{
- TypingStarted = onTypingStarted,
- SearchStarted = onSearchStarted,
- SearchFinished = onSearchFinished,
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourProvider.Background4,
},
- new Container
+ panelTarget = new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
+ Padding = new MarginPadding { Horizontal = 20 },
Children = new Drawable[]
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background4,
- },
- panelTarget = new Container
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Padding = new MarginPadding { Horizontal = 20 },
- Children = new Drawable[]
- {
- foundContent = new FillFlowContainer(),
- notFoundContent = new NotFoundDrawable(),
- }
- }
- },
- },
- }
+ foundContent = new FillFlowContainer(),
+ notFoundContent = new NotFoundDrawable(),
+ }
+ }
+ },
},
- },
- loadingLayer = new LoadingLayer(true)
+ }
};
}
+ protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
+
+ protected override Color4 BackgroundColour => ColourProvider.Background6;
+
private void onTypingStarted()
{
// temporary until the textbox/header is updated to always stay on screen.
- resultScrollContainer.ScrollToStart();
+ ScrollFlow.ScrollToStart();
}
protected override void OnFocus(FocusEvent e)
@@ -125,7 +109,7 @@ namespace osu.Game.Overlays
previewTrackManager.StopAnyPlaying(this);
if (panelTarget.Any())
- loadingLayer.Show();
+ Loading.Show();
}
private Task panelLoadDelegate;
@@ -173,7 +157,7 @@ namespace osu.Game.Overlays
private void addContentToPlaceholder(Drawable content)
{
- loadingLayer.Hide();
+ Loading.Hide();
lastFetchDisplayedTime = Time.Current;
if (content == currentContent)
@@ -267,7 +251,7 @@ namespace osu.Game.Overlays
bool shouldShowMore = panelLoadDelegate?.IsCompleted != false
&& Time.Current - lastFetchDisplayedTime > time_between_fetches
- && (resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance));
+ && (ScrollFlow.ScrollableExtent > 0 && ScrollFlow.IsScrolledToEnd(pagination_scroll_distance));
if (shouldShowMore)
filterControl.FetchNextPage();
diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs
index c16ec339bb..bdb3715e73 100644
--- a/osu.Game/Overlays/BeatmapSetOverlay.cs
+++ b/osu.Game/Overlays/BeatmapSetOverlay.cs
@@ -6,20 +6,19 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
-using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Overlays.Comments;
using osu.Game.Rulesets;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Overlays
{
- public class BeatmapSetOverlay : FullscreenOverlay
+ public class BeatmapSetOverlay : OnlineOverlay
{
public const float X_PADDING = 40;
public const float Y_PADDING = 25;
@@ -33,55 +32,27 @@ namespace osu.Game.Overlays
// receive input outside our bounds so we can trigger a close event on ourselves.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
- private readonly Box background;
-
public BeatmapSetOverlay()
- : base(OverlayColourScheme.Blue, new BeatmapSetHeader())
+ : base(OverlayColourScheme.Blue)
{
- OverlayScrollContainer scroll;
Info info;
CommentsSection comments;
- Children = new Drawable[]
+ Child = new FillFlowContainer
{
- background = new Box
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 20),
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both
- },
- scroll = new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new ReverseChildIDFillFlowContainer
+ info = new Info(),
+ new ScoresContainer
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 20),
- Children = new[]
- {
- new BeatmapSetLayoutSection
- {
- Child = new ReverseChildIDFillFlowContainer
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- Header,
- info = new Info()
- }
- },
- },
- new ScoresContainer
- {
- Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap }
- },
- comments = new CommentsSection()
- },
+ Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap }
},
- },
+ comments = new CommentsSection()
+ }
};
Header.BeatmapSet.BindTo(beatmapSet);
@@ -91,16 +62,13 @@ namespace osu.Game.Overlays
Header.HeaderContent.Picker.Beatmap.ValueChanged += b =>
{
info.Beatmap = b.NewValue;
-
- scroll.ScrollToStart();
+ ScrollFlow.ScrollToStart();
};
}
- [BackgroundDependencyLoader]
- private void load()
- {
- background.Colour = ColourProvider.Background6;
- }
+ protected override BeatmapSetHeader CreateHeader() => new BeatmapSetHeader();
+
+ protected override Color4 BackgroundColour => ColourProvider.Background6;
protected override void PopOutComplete()
{
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index c7e9a86fa4..537dd00727 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -11,68 +11,33 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Changelog;
+using osuTK.Graphics;
namespace osu.Game.Overlays
{
- public class ChangelogOverlay : FullscreenOverlay
+ public class ChangelogOverlay : OnlineOverlay
{
public readonly Bindable Current = new Bindable();
- private Container content;
-
- private SampleChannel sampleBack;
+ private Sample sampleBack;
private List builds;
protected List Streams;
public ChangelogOverlay()
- : base(OverlayColourScheme.Purple, new ChangelogHeader())
+ : base(OverlayColourScheme.Purple, false)
{
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background4,
- },
- new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new ReverseChildIDFillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- Header.With(h =>
- {
- h.ListingSelected = ShowListing;
- h.Build.BindTarget = Current;
- }),
- content = new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- }
- },
- },
- },
- };
+ Header.Build.BindTarget = Current;
sampleBack = audio.Samples.Get(@"UI/generic-select-soft");
@@ -85,6 +50,13 @@ namespace osu.Game.Overlays
});
}
+ protected override ChangelogHeader CreateHeader() => new ChangelogHeader
+ {
+ ListingSelected = ShowListing,
+ };
+
+ protected override Color4 BackgroundColour => ColourProvider.Background4;
+
public void ShowListing()
{
Current.Value = null;
@@ -198,16 +170,16 @@ namespace osu.Game.Overlays
private void loadContent(ChangelogContent newContent)
{
- content.FadeTo(0.2f, 300, Easing.OutQuint);
+ Content.FadeTo(0.2f, 300, Easing.OutQuint);
loadContentCancellation?.Cancel();
LoadComponentAsync(newContent, c =>
{
- content.FadeIn(300, Easing.OutQuint);
+ Content.FadeIn(300, Easing.OutQuint);
c.BuildSelected = ShowBuild;
- content.Child = c;
+ Child = c;
}, (loadContentCancellation = new CancellationTokenSource()).Token);
}
}
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index 8bc7e21047..28f2287514 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -24,6 +24,7 @@ using osu.Game.Overlays.Chat.Tabs;
using osuTK.Input;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Game.Online;
namespace osu.Game.Overlays
{
@@ -118,40 +119,47 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both,
},
- currentChannelContainer = new Container
+ new OnlineViewContainer("Sign in to chat")
{
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding
- {
- Bottom = textbox_height
- },
- },
- new Container
- {
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- RelativeSizeAxes = Axes.X,
- Height = textbox_height,
- Padding = new MarginPadding
- {
- Top = padding * 2,
- Bottom = padding * 2,
- Left = ChatLine.LEFT_PADDING + padding * 2,
- Right = padding * 2,
- },
Children = new Drawable[]
{
- textbox = new FocusedTextBox
+ currentChannelContainer = new Container
{
RelativeSizeAxes = Axes.Both,
- Height = 1,
- PlaceholderText = "type your message",
- ReleaseFocusOnCommit = false,
- HoldFocus = true,
- }
- }
- },
- loading = new LoadingSpinner(),
+ Padding = new MarginPadding
+ {
+ Bottom = textbox_height
+ },
+ },
+ new Container
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.X,
+ Height = textbox_height,
+ Padding = new MarginPadding
+ {
+ Top = padding * 2,
+ Bottom = padding * 2,
+ Left = ChatLine.LEFT_PADDING + padding * 2,
+ Right = padding * 2,
+ },
+ Children = new Drawable[]
+ {
+ textbox = new FocusedTextBox
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 1,
+ PlaceholderText = "type your message",
+ ReleaseFocusOnCommit = false,
+ HoldFocus = true,
+ }
+ }
+ },
+ loading = new LoadingSpinner(),
+ },
+ }
}
},
tabsArea = new TabsArea
diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
index 31aa41e967..7c47ac655f 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -9,7 +9,6 @@ using osuTK;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users.Drawables;
using osu.Game.Graphics.Containers;
-using osu.Game.Utils;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Bindables;
using System.Linq;
@@ -245,11 +244,32 @@ namespace osu.Game.Overlays.Comments
if (Comment.EditedAt.HasValue)
{
- info.Add(new OsuSpriteText
+ var font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular);
+ var colour = colourProvider.Foreground1;
+
+ info.Add(new FillFlowContainer
{
- Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
- Text = $@"edited {HumanizerUtils.Humanize(Comment.EditedAt.Value)} by {Comment.EditedUser.Username}",
- Colour = colourProvider.Foreground1
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Font = font,
+ Text = "edited ",
+ Colour = colour
+ },
+ new DrawableDate(Comment.EditedAt.Value)
+ {
+ Font = font,
+ Colour = colour
+ },
+ new OsuSpriteText
+ {
+ Font = font,
+ Text = $@" by {Comment.EditedUser.Username}",
+ Colour = colour
+ },
+ }
});
}
diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs
index 03c320debe..83ad8faf1c 100644
--- a/osu.Game/Overlays/DashboardOverlay.cs
+++ b/osu.Game/Overlays/DashboardOverlay.cs
@@ -2,155 +2,35 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Threading;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.API;
using osu.Game.Overlays.Dashboard;
using osu.Game.Overlays.Dashboard.Friends;
namespace osu.Game.Overlays
{
- public class DashboardOverlay : FullscreenOverlay
+ public class DashboardOverlay : TabbableOnlineOverlay
{
- private CancellationTokenSource cancellationToken;
-
- private Container content;
- private LoadingLayer loading;
- private OverlayScrollContainer scrollFlow;
-
public DashboardOverlay()
- : base(OverlayColourScheme.Purple, new DashboardOverlayHeader
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Depth = -float.MaxValue
- })
+ : base(OverlayColourScheme.Purple)
{
}
- private readonly IBindable apiState = new Bindable();
+ protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader();
- [BackgroundDependencyLoader]
- private void load(IAPIProvider api)
+ protected override void CreateDisplayToLoad(DashboardOverlayTabs tab)
{
- apiState.BindTo(api.State);
- apiState.BindValueChanged(onlineStateChanged, true);
-
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background5
- },
- scrollFlow = new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- Header,
- content = new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y
- }
- }
- }
- },
- loading = new LoadingLayer(true),
- };
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- Header.Current.BindValueChanged(onTabChanged);
- }
-
- private bool displayUpdateRequired = true;
-
- protected override void PopIn()
- {
- base.PopIn();
-
- // We don't want to create a new display on every call, only when exiting from fully closed state.
- if (displayUpdateRequired)
- {
- Header.Current.TriggerChange();
- displayUpdateRequired = false;
- }
- }
-
- protected override void PopOutComplete()
- {
- base.PopOutComplete();
- loadDisplay(Empty());
- displayUpdateRequired = true;
- }
-
- private void loadDisplay(Drawable display)
- {
- scrollFlow.ScrollToStart();
-
- LoadComponentAsync(display, loaded =>
- {
- if (API.IsLoggedIn)
- loading.Hide();
-
- content.Child = loaded;
- }, (cancellationToken = new CancellationTokenSource()).Token);
- }
-
- private void onTabChanged(ValueChangedEvent tab)
- {
- cancellationToken?.Cancel();
- loading.Show();
-
- if (!API.IsLoggedIn)
- {
- loadDisplay(Empty());
- return;
- }
-
- switch (tab.NewValue)
+ switch (tab)
{
case DashboardOverlayTabs.Friends:
- loadDisplay(new FriendDisplay());
+ LoadDisplay(new FriendDisplay());
break;
case DashboardOverlayTabs.CurrentlyPlaying:
- loadDisplay(new CurrentlyPlayingDisplay());
+ LoadDisplay(new CurrentlyPlayingDisplay());
break;
default:
- throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented");
+ throw new NotImplementedException($"Display for {tab} tab is not implemented");
}
}
-
- private void onlineStateChanged(ValueChangedEvent state) => Schedule(() =>
- {
- if (State.Value == Visibility.Hidden)
- return;
-
- Header.Current.TriggerChange();
- });
-
- protected override void Dispose(bool isDisposing)
- {
- cancellationToken?.Cancel();
- base.Dispose(isDisposing);
- }
}
}
diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs
index 9f9dbdbaf1..4cc17a4c14 100644
--- a/osu.Game/Overlays/DialogOverlay.cs
+++ b/osu.Game/Overlays/DialogOverlay.cs
@@ -14,6 +14,9 @@ namespace osu.Game.Overlays
{
private readonly Container dialogContainer;
+ protected override string PopInSampleName => "UI/dialog-pop-in";
+ protected override string PopOutSampleName => "UI/dialog-pop-out";
+
public PopupDialog CurrentDialog { get; private set; }
public DialogOverlay()
diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs
index 6f56d95929..735f0bcbd4 100644
--- a/osu.Game/Overlays/FullscreenOverlay.cs
+++ b/osu.Game/Overlays/FullscreenOverlay.cs
@@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osuTK.Graphics;
@@ -15,21 +17,27 @@ namespace osu.Game.Overlays
public abstract class FullscreenOverlay : WaveOverlayContainer, INamedOverlayComponent
where T : OverlayHeader
{
- public virtual string IconTexture => Header?.Title.IconTexture ?? string.Empty;
- public virtual string Title => Header?.Title.Title ?? string.Empty;
- public virtual string Description => Header?.Title.Description ?? string.Empty;
+ public virtual string IconTexture => Header.Title.IconTexture ?? string.Empty;
+ public virtual string Title => Header.Title.Title ?? string.Empty;
+ public virtual string Description => Header.Title.Description ?? string.Empty;
public T Header { get; }
+ protected virtual Color4 BackgroundColour => ColourProvider.Background5;
+
[Resolved]
protected IAPIProvider API { get; private set; }
[Cached]
protected readonly OverlayColourProvider ColourProvider;
- protected FullscreenOverlay(OverlayColourScheme colourScheme, T header)
+ protected override Container Content => content;
+
+ private readonly Container content;
+
+ protected FullscreenOverlay(OverlayColourScheme colourScheme)
{
- Header = header;
+ Header = CreateHeader();
ColourProvider = new OverlayColourProvider(colourScheme);
@@ -47,6 +55,19 @@ namespace osu.Game.Overlays
Type = EdgeEffectType.Shadow,
Radius = 10
};
+
+ base.Content.AddRange(new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = BackgroundColour
+ },
+ content = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ });
}
[BackgroundDependencyLoader]
@@ -58,6 +79,9 @@ namespace osu.Game.Overlays
Waves.FourthWaveColour = ColourProvider.Dark3;
}
+ [NotNull]
+ protected abstract T CreateHeader();
+
public override void Show()
{
if (State.Value == Visibility.Visible)
diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs
index 4425c2f168..0feae16b68 100644
--- a/osu.Game/Overlays/MedalOverlay.cs
+++ b/osu.Game/Overlays/MedalOverlay.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Overlays
private readonly Sprite innerSpin, outerSpin;
private DrawableMedal drawableMedal;
- private SampleChannel getSample;
+ private Sample getSample;
private readonly Container content;
diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs
index 8e0d1f5bbd..5e3733cd5e 100644
--- a/osu.Game/Overlays/Mods/ModButton.cs
+++ b/osu.Game/Overlays/Mods/ModButton.cs
@@ -46,8 +46,9 @@ namespace osu.Game.Overlays.Mods
/// Change the selected mod index of this button.
///
/// The new index.
+ /// Whether any settings applied to the mod should be reset on selection.
/// Whether the selection changed.
- private bool changeSelectedIndex(int newIndex)
+ private bool changeSelectedIndex(int newIndex, bool resetSettings = true)
{
if (newIndex == selectedIndex) return false;
@@ -69,6 +70,9 @@ namespace osu.Game.Overlays.Mods
Mod newSelection = SelectedMod ?? Mods[0];
+ if (resetSettings)
+ newSelection.ResetSettingsToDefaults();
+
Schedule(() =>
{
if (beforeSelected != Selected)
@@ -209,11 +213,17 @@ namespace osu.Game.Overlays.Mods
Deselect();
}
- public bool SelectAt(int index)
+ ///
+ /// Select the mod at the provided index.
+ ///
+ /// The index to select.
+ /// Whether any settings applied to the mod should be reset on selection.
+ /// Whether the selection changed.
+ public bool SelectAt(int index, bool resetSettings = true)
{
if (!Mods[index].HasImplementation) return false;
- changeSelectedIndex(index);
+ changeSelectedIndex(index, resetSettings);
return true;
}
diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs
index ecbcba7ad3..c3e56abd05 100644
--- a/osu.Game/Overlays/Mods/ModSection.cs
+++ b/osu.Game/Overlays/Mods/ModSection.cs
@@ -197,8 +197,11 @@ namespace osu.Game.Overlays.Mods
continue;
var buttonMod = button.Mods[index];
+
+ // as this is likely coming from an external change, ensure the settings of the mod are in sync.
buttonMod.CopyFrom(mod);
- button.SelectAt(index);
+
+ button.SelectAt(index, false);
return;
}
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 93fe693937..eef91deb4c 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -51,6 +51,11 @@ namespace osu.Game.Overlays.Mods
///
protected virtual bool Stacked => true;
+ ///
+ /// Whether configurable s can be configured by the local user.
+ ///
+ protected virtual bool AllowConfiguration => true;
+
[NotNull]
private Func isValidMod = m => true;
@@ -82,7 +87,7 @@ namespace osu.Game.Overlays.Mods
private const float content_width = 0.8f;
private const float footer_button_spacing = 20;
- private SampleChannel sampleOn, sampleOff;
+ private Sample sampleOn, sampleOff;
protected ModSelectOverlay()
{
@@ -300,6 +305,7 @@ namespace osu.Game.Overlays.Mods
Text = "Customisation",
Action = () => ModSettingsContainer.ToggleVisibility(),
Enabled = { Value = false },
+ Alpha = AllowConfiguration ? 1 : 0,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
@@ -372,7 +378,10 @@ namespace osu.Game.Overlays.Mods
base.LoadComplete();
availableMods.BindValueChanged(_ => updateAvailableMods(), true);
- SelectedMods.BindValueChanged(_ => updateSelectedButtons(), true);
+
+ // intentionally bound after the above line to avoid a potential update feedback cycle.
+ // i haven't actually observed this happening but as updateAvailableMods() changes the selection it is plausible.
+ SelectedMods.BindValueChanged(_ => updateSelectedButtons());
}
protected override void PopOut()
@@ -479,10 +488,10 @@ namespace osu.Game.Overlays.Mods
foreach (var section in ModSectionsContainer.Children)
section.UpdateSelectedButtons(selectedMods);
- updateMods();
+ updateMultiplier();
}
- private void updateMods()
+ private void updateMultiplier()
{
var multiplier = 1.0;
@@ -509,7 +518,8 @@ namespace osu.Game.Overlays.Mods
OnModSelected(selectedMod);
- if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show();
+ if (selectedMod.RequiresConfiguration && AllowConfiguration)
+ ModSettingsContainer.Show();
}
else
{
diff --git a/osu.Game/Overlays/Mods/ModSettingsContainer.cs b/osu.Game/Overlays/Mods/ModSettingsContainer.cs
index 1c57ff54ad..64d65cab3b 100644
--- a/osu.Game/Overlays/Mods/ModSettingsContainer.cs
+++ b/osu.Game/Overlays/Mods/ModSettingsContainer.cs
@@ -52,6 +52,7 @@ namespace osu.Game.Overlays.Mods
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
+ ScrollbarVisible = false,
Child = modSettingsContent = new FillFlowContainer
{
Anchor = Anchor.TopCentre,
diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs
index 5820d405d4..5beb285216 100644
--- a/osu.Game/Overlays/NewsOverlay.cs
+++ b/osu.Game/Overlays/NewsOverlay.cs
@@ -2,67 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using System.Threading;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.News;
using osu.Game.Overlays.News.Displays;
namespace osu.Game.Overlays
{
- public class NewsOverlay : FullscreenOverlay
+ public class NewsOverlay : OnlineOverlay
{
private readonly Bindable article = new Bindable(null);
- private Container content;
- private LoadingLayer loading;
- private OverlayScrollContainer scrollFlow;
-
public NewsOverlay()
- : base(OverlayColourScheme.Purple, new NewsHeader())
+ : base(OverlayColourScheme.Purple, false)
{
}
- [BackgroundDependencyLoader]
- private void load()
- {
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background5,
- },
- scrollFlow = new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- Header.With(h =>
- {
- h.ShowFrontPage = ShowFrontPage;
- }),
- content = new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- }
- },
- },
- },
- loading = new LoadingLayer(true),
- };
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
@@ -71,6 +26,11 @@ namespace osu.Game.Overlays
article.BindValueChanged(onArticleChanged);
}
+ protected override NewsHeader CreateHeader() => new NewsHeader
+ {
+ ShowFrontPage = ShowFrontPage
+ };
+
private bool displayUpdateRequired = true;
protected override void PopIn()
@@ -107,7 +67,7 @@ namespace osu.Game.Overlays
private void onArticleChanged(ValueChangedEvent e)
{
cancellationToken?.Cancel();
- loading.Show();
+ Loading.Show();
if (e.NewValue == null)
{
@@ -122,11 +82,11 @@ namespace osu.Game.Overlays
protected void LoadDisplay(Drawable display)
{
- scrollFlow.ScrollToStart();
+ ScrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
- content.Child = loaded;
- loading.Hide();
+ Child = loaded;
+ Loading.Hide();
}, (cancellationToken = new CancellationTokenSource()).Token);
}
diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs
index 2dc6b39a92..d1a97c74b2 100644
--- a/osu.Game/Overlays/Notifications/Notification.cs
+++ b/osu.Game/Overlays/Notifications/Notification.cs
@@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -40,6 +42,11 @@ namespace osu.Game.Overlays.Notifications
///
public virtual bool DisplayOnTop => true;
+ private Sample samplePopIn;
+ private Sample samplePopOut;
+ protected virtual string PopInSampleName => "UI/notification-pop-in";
+ protected virtual string PopOutSampleName => "UI/overlay-pop-out"; // TODO: replace with a unique sample?
+
protected NotificationLight Light;
private readonly CloseButton closeButton;
protected Container IconContent;
@@ -107,7 +114,7 @@ namespace osu.Game.Overlays.Notifications
closeButton = new CloseButton
{
Alpha = 0,
- Action = Close,
+ Action = () => Close(),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding
@@ -120,6 +127,13 @@ namespace osu.Game.Overlays.Notifications
});
}
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio)
+ {
+ samplePopIn = audio.Samples.Get(PopInSampleName);
+ samplePopOut = audio.Samples.Get(PopOutSampleName);
+ }
+
protected override bool OnHover(HoverEvent e)
{
closeButton.FadeIn(75);
@@ -143,6 +157,9 @@ namespace osu.Game.Overlays.Notifications
protected override void LoadComplete()
{
base.LoadComplete();
+
+ samplePopIn?.Play();
+
this.FadeInFromZero(200);
NotificationContent.MoveToX(DrawSize.X);
NotificationContent.MoveToX(0, 500, Easing.OutQuint);
@@ -150,12 +167,15 @@ namespace osu.Game.Overlays.Notifications
public bool WasClosed;
- public virtual void Close()
+ public virtual void Close(bool playSound = true)
{
if (WasClosed) return;
WasClosed = true;
+ if (playSound)
+ samplePopOut?.Play();
+
Closed?.Invoke();
this.FadeOut(100);
Expire();
diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs
index c2a958b65e..38ba712254 100644
--- a/osu.Game/Overlays/Notifications/NotificationSection.cs
+++ b/osu.Game/Overlays/Notifications/NotificationSection.cs
@@ -109,7 +109,12 @@ namespace osu.Game.Overlays.Notifications
private void clearAll()
{
- notifications.Children.ForEach(c => c.Close());
+ bool first = true;
+ notifications.Children.ForEach(c =>
+ {
+ c.Close(first);
+ first = false;
+ });
}
protected override void Update()
diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
index 3105ecd742..703c14af2b 100644
--- a/osu.Game/Overlays/Notifications/ProgressNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -150,12 +150,12 @@ namespace osu.Game.Overlays.Notifications
colourCancelled = colours.Red;
}
- public override void Close()
+ public override void Close(bool playSound = true)
{
switch (State)
{
case ProgressNotificationState.Cancelled:
- base.Close();
+ base.Close(playSound);
break;
case ProgressNotificationState.Active:
diff --git a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs
new file mode 100644
index 0000000000..13c9c5a02d
--- /dev/null
+++ b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs
@@ -0,0 +1,17 @@
+// 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.Graphics.Sprites;
+
+namespace osu.Game.Overlays.Notifications
+{
+ public class SimpleErrorNotification : SimpleNotification
+ {
+ protected override string PopInSampleName => "UI/error-notification-pop-in";
+
+ public SimpleErrorNotification()
+ {
+ Icon = FontAwesome.Solid.Bomb;
+ }
+ }
+}
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index 9beb859f28..2866d2ad6d 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -51,6 +51,9 @@ namespace osu.Game.Overlays
private Container dragContainer;
private Container playerContainer;
+ protected override string PopInSampleName => "UI/now-playing-pop-in";
+ protected override string PopOutSampleName => "UI/now-playing-pop-out";
+
///
/// Provide a source for the toolbar height.
///
@@ -84,11 +87,6 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
- playlist = new PlaylistOverlay
- {
- RelativeSizeAxes = Axes.X,
- Y = player_height + 10,
- },
playerContainer = new Container
{
RelativeSizeAxes = Axes.X,
@@ -171,7 +169,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.CentreRight,
Position = new Vector2(-bottom_black_area_height / 2, 0),
Icon = FontAwesome.Solid.Bars,
- Action = () => playlist.ToggleVisibility(),
+ Action = togglePlaylist
},
}
},
@@ -191,13 +189,35 @@ namespace osu.Game.Overlays
};
}
+ private void togglePlaylist()
+ {
+ if (playlist == null)
+ {
+ LoadComponentAsync(playlist = new PlaylistOverlay
+ {
+ RelativeSizeAxes = Axes.X,
+ Y = player_height + 10,
+ }, _ =>
+ {
+ dragContainer.Add(playlist);
+
+ playlist.BeatmapSets.BindTo(musicController.BeatmapSets);
+ playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
+
+ togglePlaylist();
+ });
+
+ return;
+ }
+
+ if (!beatmap.Disabled)
+ playlist.ToggleVisibility();
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
- playlist.BeatmapSets.BindTo(musicController.BeatmapSets);
- playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
-
beatmap.BindDisabledChanged(beatmapDisabledChanged, true);
musicController.TrackChanged += trackChanged;
@@ -306,7 +326,7 @@ namespace osu.Game.Overlays
private void beatmapDisabledChanged(bool disabled)
{
if (disabled)
- playlist.Hide();
+ playlist?.Hide();
prevButton.Enabled.Value = !disabled;
nextButton.Enabled.Value = !disabled;
@@ -411,6 +431,11 @@ namespace osu.Game.Overlays
private class HoverableProgressBar : ProgressBar
{
+ public HoverableProgressBar()
+ : base(true)
+ {
+ }
+
protected override bool OnHover(HoverEvent e)
{
this.ResizeHeightTo(progress_height, 500, Easing.OutQuint);
diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
index 8e8a99a0a7..51214fe460 100644
--- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs
+++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
@@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -19,6 +21,13 @@ namespace osu.Game.Overlays.OSD
{
private const int lights_bottom_margin = 40;
+ private readonly int optionCount;
+ private readonly int selectedOption = -1;
+
+ private Sample sampleOn;
+ private Sample sampleOff;
+ private Sample sampleChange;
+
public TrackedSettingToast(SettingDescription description)
: base(description.Name, description.Value, description.Shortcut)
{
@@ -46,9 +55,6 @@ namespace osu.Game.Overlays.OSD
}
};
- int optionCount = 0;
- int selectedOption = -1;
-
switch (description.RawValue)
{
case bool val:
@@ -69,6 +75,34 @@ namespace osu.Game.Overlays.OSD
optionLights.Add(new OptionLight { Glowing = i == selectedOption });
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (optionCount == 1)
+ {
+ if (selectedOption == 0)
+ sampleOn?.Play();
+ else
+ sampleOff?.Play();
+ }
+ else
+ {
+ if (sampleChange == null) return;
+
+ sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f;
+ sampleChange.Play();
+ }
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio)
+ {
+ sampleOn = audio.Samples.Get("UI/osd-on");
+ sampleOff = audio.Samples.Get("UI/osd-off");
+ sampleChange = audio.Samples.Get("UI/osd-change");
+ }
+
private class OptionLight : Container
{
private Color4 glowingColour, idleColour;
diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs
new file mode 100644
index 0000000000..de33e4a1bc
--- /dev/null
+++ b/osu.Game/Overlays/OnlineOverlay.cs
@@ -0,0 +1,57 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online;
+
+namespace osu.Game.Overlays
+{
+ public abstract class OnlineOverlay : FullscreenOverlay
+ where T : OverlayHeader
+ {
+ protected override Container Content => content;
+
+ protected readonly OverlayScrollContainer ScrollFlow;
+ protected readonly LoadingLayer Loading;
+ private readonly Container content;
+
+ protected OnlineOverlay(OverlayColourScheme colourScheme, bool requiresSignIn = true)
+ : base(colourScheme)
+ {
+ var mainContent = requiresSignIn
+ ? new OnlineViewContainer($"Sign in to view the {Header.Title.Title}")
+ : new Container();
+
+ mainContent.RelativeSizeAxes = Axes.Both;
+
+ mainContent.AddRange(new Drawable[]
+ {
+ ScrollFlow = new OverlayScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ScrollbarVisible = false,
+ Child = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ Header.With(h => h.Depth = float.MinValue),
+ content = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }
+ }
+ }
+ },
+ Loading = new LoadingLayer(true)
+ });
+
+ base.Content.Add(mainContent);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs
index 25350e310a..a093969115 100644
--- a/osu.Game/Overlays/RankingsOverlay.cs
+++ b/osu.Game/Overlays/RankingsOverlay.cs
@@ -4,96 +4,32 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays.Rankings;
using osu.Game.Users;
using osu.Game.Rulesets;
using osu.Game.Online.API;
-using System.Threading;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Rankings.Tables;
namespace osu.Game.Overlays
{
- public class RankingsOverlay : FullscreenOverlay
+ public class RankingsOverlay : TabbableOnlineOverlay
{
protected Bindable Country => Header.Country;
- protected Bindable Scope => Header.Current;
-
- private readonly OverlayScrollContainer scrollFlow;
- private readonly Container contentContainer;
- private readonly LoadingLayer loading;
- private readonly Box background;
-
private APIRequest lastRequest;
- private CancellationTokenSource cancellationToken;
[Resolved]
private IAPIProvider api { get; set; }
- public RankingsOverlay()
- : base(OverlayColourScheme.Green, new RankingsOverlayHeader
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Depth = -float.MaxValue
- })
- {
- loading = new LoadingLayer(true);
-
- Children = new Drawable[]
- {
- background = new Box
- {
- RelativeSizeAxes = Axes.Both
- },
- scrollFlow = new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- Header,
- new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Children = new Drawable[]
- {
- contentContainer = new Container
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Margin = new MarginPadding { Bottom = 10 }
- },
- }
- }
- }
- }
- },
- loading
- };
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- background.Colour = ColourProvider.Background5;
- }
-
[Resolved]
private Bindable ruleset { get; set; }
+ public RankingsOverlay()
+ : base(OverlayColourScheme.Green)
+ {
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -104,31 +40,33 @@ namespace osu.Game.Overlays
{
// if a country is requested, force performance scope.
if (Country.Value != null)
- Scope.Value = RankingsScope.Performance;
+ Header.Current.Value = RankingsScope.Performance;
- Scheduler.AddOnce(loadNewContent);
- });
-
- Scope.BindValueChanged(_ =>
- {
- // country filtering is only valid for performance scope.
- if (Scope.Value != RankingsScope.Performance)
- Country.Value = null;
-
- Scheduler.AddOnce(loadNewContent);
+ Scheduler.AddOnce(triggerTabChanged);
});
ruleset.BindValueChanged(_ =>
{
- if (Scope.Value == RankingsScope.Spotlights)
+ if (Header.Current.Value == RankingsScope.Spotlights)
return;
- Scheduler.AddOnce(loadNewContent);
+ Scheduler.AddOnce(triggerTabChanged);
});
-
- Scheduler.AddOnce(loadNewContent);
}
+ protected override void OnTabChanged(RankingsScope tab)
+ {
+ // country filtering is only valid for performance scope.
+ if (Header.Current.Value != RankingsScope.Performance)
+ Country.Value = null;
+
+ Scheduler.AddOnce(triggerTabChanged);
+ }
+
+ private void triggerTabChanged() => base.OnTabChanged(Header.Current.Value);
+
+ protected override RankingsOverlayHeader CreateHeader() => new RankingsOverlayHeader();
+
public void ShowCountry(Country requested)
{
if (requested == null)
@@ -139,22 +77,13 @@ namespace osu.Game.Overlays
Country.Value = requested;
}
- public void ShowSpotlights()
+ protected override void CreateDisplayToLoad(RankingsScope tab)
{
- Scope.Value = RankingsScope.Spotlights;
- Show();
- }
-
- private void loadNewContent()
- {
- loading.Show();
-
- cancellationToken?.Cancel();
lastRequest?.Cancel();
- if (Scope.Value == RankingsScope.Spotlights)
+ if (Header.Current.Value == RankingsScope.Spotlights)
{
- loadContent(new SpotlightsLayout
+ LoadDisplay(new SpotlightsLayout
{
Ruleset = { BindTarget = ruleset }
});
@@ -166,19 +95,19 @@ namespace osu.Game.Overlays
if (request == null)
{
- loadContent(null);
+ LoadDisplay(Empty());
return;
}
- request.Success += () => Schedule(() => loadContent(createTableFromResponse(request)));
- request.Failure += _ => Schedule(() => loadContent(null));
+ request.Success += () => Schedule(() => LoadDisplay(createTableFromResponse(request)));
+ request.Failure += _ => Schedule(() => LoadDisplay(Empty()));
api.Queue(request);
}
private APIRequest createScopedRequest()
{
- switch (Scope.Value)
+ switch (Header.Current.Value)
{
case RankingsScope.Performance:
return new GetUserRankingsRequest(ruleset.Value, country: Country.Value?.FlagName);
@@ -216,29 +145,9 @@ namespace osu.Game.Overlays
return null;
}
- private void loadContent(Drawable content)
- {
- scrollFlow.ScrollToStart();
-
- if (content == null)
- {
- contentContainer.Clear();
- loading.Hide();
- return;
- }
-
- LoadComponentAsync(content, loaded =>
- {
- loading.Hide();
- contentContainer.Child = loaded;
- }, (cancellationToken = new CancellationTokenSource()).Token);
- }
-
protected override void Dispose(bool isDisposing)
{
lastRequest?.Cancel();
- cancellationToken?.Cancel();
-
base.Dispose(isDisposing);
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs
index 278479e04f..af225889da 100644
--- a/osu.Game/Overlays/Settings/SettingsItem.cs
+++ b/osu.Game/Overlays/Settings/SettingsItem.cs
@@ -21,7 +21,7 @@ using osuTK;
namespace osu.Game.Overlays.Settings
{
- public abstract class SettingsItem : Container, IFilterable, ISettingsItem, IHasCurrentValue
+ public abstract class SettingsItem : Container, IFilterable, ISettingsItem, IHasCurrentValue, IHasTooltip
{
protected abstract Drawable CreateControl();
@@ -37,6 +37,8 @@ namespace osu.Game.Overlays.Settings
public bool ShowsDefaultIndicator = true;
+ public string TooltipText { get; set; }
+
public virtual string LabelText
{
get => labelText?.Text ?? string.Empty;
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index 7a5a586f67..f1270f750e 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -40,6 +40,8 @@ namespace osu.Game.Overlays
private SeekLimitedSearchTextBox searchTextBox;
+ protected override string PopInSampleName => "UI/settings-pop-in";
+
///
/// Provide a source for the toolbar height.
///
diff --git a/osu.Game/Overlays/TabbableOnlineOverlay.cs b/osu.Game/Overlays/TabbableOnlineOverlay.cs
new file mode 100644
index 0000000000..9ceab12d3d
--- /dev/null
+++ b/osu.Game/Overlays/TabbableOnlineOverlay.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 System.Threading;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
+
+namespace osu.Game.Overlays
+{
+ public abstract class TabbableOnlineOverlay : OnlineOverlay
+ where THeader : TabControlOverlayHeader
+ {
+ private readonly IBindable apiState = new Bindable();
+
+ private CancellationTokenSource cancellationToken;
+ private bool displayUpdateRequired = true;
+
+ protected TabbableOnlineOverlay(OverlayColourScheme colourScheme)
+ : base(colourScheme)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IAPIProvider api)
+ {
+ apiState.BindTo(api.State);
+ apiState.BindValueChanged(onlineStateChanged, true);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Header.Current.BindValueChanged(tab => OnTabChanged(tab.NewValue));
+ }
+
+ protected override void PopIn()
+ {
+ base.PopIn();
+
+ // We don't want to create a new display on every call, only when exiting from fully closed state.
+ if (displayUpdateRequired)
+ {
+ Header.Current.TriggerChange();
+ displayUpdateRequired = false;
+ }
+ }
+
+ protected override void PopOutComplete()
+ {
+ base.PopOutComplete();
+ LoadDisplay(Empty());
+ displayUpdateRequired = true;
+ }
+
+ protected void LoadDisplay(Drawable display)
+ {
+ ScrollFlow.ScrollToStart();
+
+ LoadComponentAsync(display, loaded =>
+ {
+ Loading.Hide();
+
+ Child = loaded;
+ }, (cancellationToken = new CancellationTokenSource()).Token);
+ }
+
+ protected virtual void OnTabChanged(TEnum tab)
+ {
+ cancellationToken?.Cancel();
+ Loading.Show();
+
+ if (!API.IsLoggedIn)
+ {
+ LoadDisplay(Empty());
+ return;
+ }
+
+ CreateDisplayToLoad(tab);
+ }
+
+ protected abstract void CreateDisplayToLoad(TEnum tab);
+
+ private void onlineStateChanged(ValueChangedEvent state) => Schedule(() =>
+ {
+ if (State.Value == Visibility.Hidden)
+ return;
+
+ Header.Current.TriggerChange();
+ });
+
+ protected override void Dispose(bool isDisposing)
+ {
+ cancellationToken?.Cancel();
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
index 49b9c62d85..83f2bdf6cb 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Toolbar
private KeyBindingStore keyBindings { get; set; }
protected ToolbarButton()
- : base(HoverSampleSet.Loud)
+ : base(HoverSampleSet.Toolbar)
{
Width = Toolbar.HEIGHT;
RelativeSizeAxes = Axes.Y;
diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index 7f29545c2e..299a14b250 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -15,6 +15,7 @@ using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Sections;
using osu.Game.Users;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Overlays
{
@@ -29,10 +30,14 @@ namespace osu.Game.Overlays
public const float CONTENT_X_MARGIN = 70;
public UserProfileOverlay()
- : base(OverlayColourScheme.Pink, new ProfileHeader())
+ : base(OverlayColourScheme.Pink)
{
}
+ protected override ProfileHeader CreateHeader() => new ProfileHeader();
+
+ protected override Color4 BackgroundColour => ColourProvider.Background6;
+
public void ShowUser(int userId) => ShowUser(new User { Id = userId });
public void ShowUser(User user, bool fetchOnline = true)
@@ -72,12 +77,6 @@ namespace osu.Game.Overlays
Origin = Anchor.TopCentre,
};
- Add(new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background6
- });
-
Add(sectionsContainer = new ProfileSectionsContainer
{
ExpandableHeader = Header,
diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs
index 07accf8820..5b997bbd05 100644
--- a/osu.Game/Overlays/Volume/VolumeMeter.cs
+++ b/osu.Game/Overlays/Volume/VolumeMeter.cs
@@ -176,6 +176,7 @@ namespace osu.Game.Overlays.Volume
}
}
};
+
Bindable.ValueChanged += volume =>
{
this.TransformTo("DisplayVolume",
@@ -183,6 +184,7 @@ namespace osu.Game.Overlays.Volume
400,
Easing.OutQuint);
};
+
bgProgress.Current.Value = 0.75f;
}
diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs
index d0fa9987d5..52ae4dbdbb 100644
--- a/osu.Game/Overlays/WaveOverlayContainer.cs
+++ b/osu.Game/Overlays/WaveOverlayContainer.cs
@@ -18,6 +18,8 @@ namespace osu.Game.Overlays
protected override bool StartHidden => true;
+ protected override string PopInSampleName => "UI/wave-pop-in";
+
protected WaveOverlayContainer()
{
AddInternal(Waves = new WaveContainer
diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs
index 7999023998..a4179c94da 100644
--- a/osu.Game/PerformFromMenuRunner.cs
+++ b/osu.Game/PerformFromMenuRunner.cs
@@ -82,7 +82,9 @@ namespace osu.Game
game?.CloseAllOverlays(false);
// we may already be at the target screen type.
- if (validScreens.Contains(current.GetType()) && !beatmap.Disabled)
+ var type = current.GetType();
+
+ if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled)
{
finalAction(current);
Cancel();
@@ -91,13 +93,14 @@ namespace osu.Game
while (current != null)
{
- if (validScreens.Contains(current.GetType()))
+ if (validScreens.Any(t => t.IsAssignableFrom(type)))
{
current.MakeCurrent();
break;
}
current = current.GetParentScreen();
+ type = current?.GetType();
}
}
diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
index c0eb891f5e..bfff93e7c5 100644
--- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
+using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
@@ -45,6 +46,9 @@ namespace osu.Game.Rulesets.Edit
{
HitObject = hitObject;
+ // adding the default hit sample should be the case regardless of the ruleset.
+ HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
+
RelativeSizeAxes = Axes.Both;
// This is required to allow the blueprint's position to be updated via OnMouseMove/Handle
diff --git a/osu.Game/Rulesets/Mods/IApplicableToRate.cs b/osu.Game/Rulesets/Mods/IApplicableToRate.cs
new file mode 100644
index 0000000000..f613867132
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/IApplicableToRate.cs
@@ -0,0 +1,20 @@
+// 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.Rulesets.Mods
+{
+ ///
+ /// Interface that should be implemented by mods that affect the track playback speed,
+ /// and in turn, values of the track rate.
+ ///
+ public interface IApplicableToRate : IApplicableToAudio
+ {
+ ///
+ /// Returns the playback rate at after this mod is applied.
+ ///
+ /// The time instant at which the playback rate is queried.
+ /// The playback rate before applying this mod.
+ /// The playback rate after applying this mod.
+ double ApplyToRate(double time, double rate = 1);
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index 3a8717e678..2a11c92223 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mods
}
///
- /// Copies mod setting values from into this instance.
+ /// Copies mod setting values from into this instance, overwriting all existing settings.
///
/// The mod to copy properties from.
public void CopyFrom(Mod source)
@@ -147,9 +147,7 @@ namespace osu.Game.Rulesets.Mods
var targetBindable = (IBindable)prop.GetValue(this);
var sourceBindable = (IBindable)prop.GetValue(source);
- // we only care about changes that have been made away from defaults.
- if (!sourceBindable.IsDefault)
- CopyAdjustedSetting(targetBindable, sourceBindable);
+ CopyAdjustedSetting(targetBindable, sourceBindable);
}
}
@@ -175,5 +173,10 @@ namespace osu.Game.Rulesets.Mods
}
public bool Equals(IMod other) => GetType() == other?.GetType();
+
+ ///
+ /// Reset all custom settings for this mod back to their defaults.
+ ///
+ public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType()));
}
}
diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs
index 945dd444be..d1d23def67 100644
--- a/osu.Game/Rulesets/Mods/ModAutoplay.cs
+++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@@ -15,7 +16,10 @@ namespace osu.Game.Rulesets.Mods
public abstract class ModAutoplay : ModAutoplay, IApplicableToDrawableRuleset
where T : HitObject
{
- public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) => drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap));
+ public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
+ }
}
public abstract class ModAutoplay : Mod, IApplicableFailOverride
@@ -35,6 +39,11 @@ namespace osu.Game.Rulesets.Mods
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
+ [Obsolete("Use the mod-supporting override")] // can be removed 20210731
public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() };
+
+#pragma warning disable 618
+ public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => CreateReplayScore(beatmap);
+#pragma warning restore 618
}
}
diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs
index bee9e56edd..eb0473016a 100644
--- a/osu.Game/Rulesets/Mods/ModCinema.cs
+++ b/osu.Game/Rulesets/Mods/ModCinema.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
{
public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap));
+ drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
// AlwaysPresent required for hitsounds
drawableRuleset.Playfield.AlwaysPresent = true;
diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
index a531e885db..b70eee4e1d 100644
--- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mods
protected const int LAST_SETTING_ORDER = 2;
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)]
- public BindableNumber DrainRate { get; } = new BindableFloat
+ public BindableNumber DrainRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mods
};
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)]
- public BindableNumber OverallDifficulty { get; } = new BindableFloat
+ public BindableNumber OverallDifficulty { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@@ -53,6 +53,24 @@ namespace osu.Game.Rulesets.Mods
Value = 5,
};
+ [SettingSource("Extended Limits", "Adjust difficulty beyond sane limits.")]
+ public BindableBool ExtendedLimits { get; } = new BindableBool();
+
+ protected ModDifficultyAdjust()
+ {
+ ExtendedLimits.BindValueChanged(extend => ApplyLimits(extend.NewValue));
+ }
+
+ ///
+ /// Changes the difficulty adjustment limits. Occurs when the value of is changed.
+ ///
+ /// Whether limits should extend beyond sane ranges.
+ protected virtual void ApplyLimits(bool extended)
+ {
+ DrainRate.MaxValue = extended ? 11 : 10;
+ OverallDifficulty.MaxValue = extended ? 11 : 10;
+ }
+
public override string SettingDescription
{
get
@@ -141,5 +159,73 @@ namespace osu.Game.Rulesets.Mods
ApplySetting(DrainRate, dr => difficulty.DrainRate = dr);
ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od);
}
+
+ public override void ResetSettingsToDefaults()
+ {
+ base.ResetSettingsToDefaults();
+
+ if (difficulty != null)
+ {
+ // base implementation potentially overwrite modified defaults that came from a beatmap selection.
+ TransferSettings(difficulty);
+ }
+ }
+
+ ///
+ /// A that extends its min/max values to support any assigned value.
+ ///
+ protected class BindableDoubleWithLimitExtension : BindableDouble
+ {
+ public override double Value
+ {
+ get => base.Value;
+ set
+ {
+ if (value < MinValue)
+ MinValue = value;
+ if (value > MaxValue)
+ MaxValue = value;
+ base.Value = value;
+ }
+ }
+ }
+
+ ///
+ /// A that extends its min/max values to support any assigned value.
+ ///
+ protected class BindableFloatWithLimitExtension : BindableFloat
+ {
+ public override float Value
+ {
+ get => base.Value;
+ set
+ {
+ if (value < MinValue)
+ MinValue = value;
+ if (value > MaxValue)
+ MaxValue = value;
+ base.Value = value;
+ }
+ }
+ }
+
+ ///
+ /// A that extends its min/max values to support any assigned value.
+ ///
+ protected class BindableIntWithLimitExtension : BindableInt
+ {
+ public override int Value
+ {
+ get => base.Value;
+ set
+ {
+ if (value < MinValue)
+ MinValue = value;
+ if (value > MaxValue)
+ MaxValue = value;
+ base.Value = value;
+ }
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs
index 2150b0fb68..b016a6d43b 100644
--- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs
@@ -8,7 +8,7 @@ using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModRateAdjust : Mod, IApplicableToAudio
+ public abstract class ModRateAdjust : Mod, IApplicableToRate
{
public abstract BindableNumber SpeedChange { get; }
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mods
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
+ public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value;
+
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
}
}
diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
index b6916c838e..330945d3d3 100644
--- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs
+++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
@@ -14,7 +14,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToAudio
+ public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToRate
{
///
/// The point in the beatmap at which the final ramping rate should be reached.
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mods
protected ModTimeRamp()
{
// for preview purpose at song select. eventually we'll want to be able to update every frame.
- FinalRate.BindValueChanged(val => applyRateAdjustment(1), true);
+ FinalRate.BindValueChanged(val => applyRateAdjustment(double.PositiveInfinity), true);
AdjustPitch.BindValueChanged(applyPitchAdjustment);
}
@@ -75,17 +75,24 @@ namespace osu.Game.Rulesets.Mods
finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart);
}
+ public double ApplyToRate(double time, double rate = 1)
+ {
+ double amount = (time - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime);
+ double ramp = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1);
+
+ // round the end result to match the bindable SpeedChange's precision, in case this is called externally.
+ return rate * Math.Round(ramp, 2);
+ }
+
public virtual void Update(Playfield playfield)
{
- applyRateAdjustment((track.CurrentTime - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime));
+ applyRateAdjustment(track.CurrentTime);
}
///
- /// Adjust the rate along the specified ramp
+ /// Adjust the rate along the specified ramp.
///
- /// The amount of adjustment to apply (from 0..1).
- private void applyRateAdjustment(double amount) =>
- SpeedChange.Value = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1);
+ private void applyRateAdjustment(double time) => SpeedChange.Value = ApplyToRate(time);
private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting)
{
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 6940e43e5b..ca27e6b21a 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.UI
protected IRulesetConfigManager Config { get; private set; }
[Cached(typeof(IReadOnlyList))]
- protected override IReadOnlyList Mods { get; }
+ public sealed override IReadOnlyList Mods { get; }
private FrameStabilityContainer frameStabilityContainer;
@@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.UI
///
/// The mods which are to be applied.
///
- protected abstract IReadOnlyList Mods { get; }
+ public abstract IReadOnlyList Mods { get; }
/// ~
/// The associated ruleset.
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index 81ec73a6c5..deec948d14 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -102,9 +102,9 @@ namespace osu.Game.Rulesets.UI
this.fallback = fallback;
}
- public SampleChannel Get(string name) => primary.Get(name) ?? fallback.Get(name);
+ public Sample Get(string name) => primary.Get(name) ?? fallback.Get(name);
- public Task GetAsync(string name) => primary.GetAsync(name) ?? fallback.GetAsync(name);
+ public Task GetAsync(string name) => primary.GetAsync(name) ?? fallback.GetAsync(name);
public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.GetStream(name);
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index cf1d123c06..a6beb19876 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -71,8 +71,9 @@ namespace osu.Game.Scoring
}
}
- protected override IEnumerable GetStableImportPaths(Storage stableStorage)
- => stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false));
+ protected override IEnumerable GetStableImportPaths(Storage storage)
+ => storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false))
+ .Select(path => storage.GetFullPath(path));
public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store);
diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs
index c81362eebe..48c5523883 100644
--- a/osu.Game/Screens/BackgroundScreen.cs
+++ b/osu.Game/Screens/BackgroundScreen.cs
@@ -68,15 +68,19 @@ namespace osu.Game.Screens
public override bool OnExiting(IScreen next)
{
- this.FadeOut(transition_length, Easing.OutExpo);
- this.MoveToX(x_movement_amount, transition_length, Easing.OutExpo);
+ if (IsLoaded)
+ {
+ this.FadeOut(transition_length, Easing.OutExpo);
+ this.MoveToX(x_movement_amount, transition_length, Easing.OutExpo);
+ }
return base.OnExiting(next);
}
public override void OnResuming(IScreen last)
{
- this.MoveToX(0, transition_length, Easing.OutExpo);
+ if (IsLoaded)
+ this.MoveToX(0, transition_length, Easing.OutExpo);
base.OnResuming(last);
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index c09b935f28..79f457c050 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -211,9 +211,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (blueprint != null)
{
- // doing this post-creations as adding the default hit sample should be the case regardless of the ruleset.
- blueprint.HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
-
placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs
index be6ed9700c..d956394ebb 100644
--- a/osu.Game/Screens/Menu/Button.cs
+++ b/osu.Game/Screens/Menu/Button.cs
@@ -45,8 +45,8 @@ namespace osu.Game.Screens.Menu
public ButtonSystemState VisibleState = ButtonSystemState.TopLevel;
private readonly Action clickAction;
- private SampleChannel sampleClick;
- private SampleChannel sampleHover;
+ private Sample sampleClick;
+ private Sample sampleHover;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index f400b2114b..81b1cb0bf1 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Screens.Menu
private readonly List