diff --git a/osu.Android.props b/osu.Android.props
index 7060e88026..d88a11257d 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
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.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/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.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/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/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/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/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/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.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/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/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/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/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.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..025abfcbc6 100644
--- a/osu.Game.Tournament.Tests/TournamentTestScene.cs
+++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs
@@ -154,10 +154,13 @@ 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);
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..4224da4bbe 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);
+ });
}
///
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/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/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/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
public virtual bool DisplayOnTop => true;
+ private SampleChannel samplePopIn;
+ private SampleChannel 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 5c16a6e5c4..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.
///
diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
index 8e8a99a0a7..d61180baa2 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 SampleChannel sampleOn;
+ private SampleChannel sampleOff;
+ private SampleChannel 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/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/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/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/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/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 dbc35569e7..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
@@ -152,5 +170,62 @@ namespace osu.Game.Rulesets.Mods
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/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/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/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index f400b2114b..c6774127c1 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -129,7 +129,7 @@ namespace osu.Game.Screens.Menu
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
- buttonsTopLevel.Add(new Button(@"edit", @"button-generic-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
+ buttonsTopLevel.Add(new Button(@"edit", @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new Button(@"browse", @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
if (host.CanExit)
diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs
index abb83f894a..d454d85d9e 100644
--- a/osu.Game/Screens/Menu/IntroWelcome.cs
+++ b/osu.Game/Screens/Menu/IntroWelcome.cs
@@ -67,6 +67,10 @@ namespace osu.Game.Screens.Menu
{
StartTrack();
+ // this classic intro loops forever.
+ if (UsingThemedIntro)
+ Track.Looping = true;
+
const float fade_in_time = 200;
logo.ScaleTo(1);
diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs
index 68d23e1a32..1d0af30275 100644
--- a/osu.Game/Screens/Menu/OsuLogo.cs
+++ b/osu.Game/Screens/Menu/OsuLogo.cs
@@ -45,6 +45,7 @@ namespace osu.Game.Screens.Menu
private SampleChannel sampleClick;
private SampleChannel sampleBeat;
+ private SampleChannel sampleDownbeat;
private readonly Container colourAndTriangles;
private readonly Triangles triangles;
@@ -259,6 +260,7 @@ namespace osu.Game.Screens.Menu
{
sampleClick = audio.Samples.Get(@"Menu/osu-logo-select");
sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat");
+ sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat");
logo.Texture = textures.Get(@"Menu/logo");
ripple.Texture = textures.Get(@"Menu/logo");
@@ -281,7 +283,15 @@ namespace osu.Game.Screens.Menu
if (beatIndex < 0) return;
if (IsHovered)
- this.Delay(early_activation).Schedule(() => sampleBeat.Play());
+ {
+ this.Delay(early_activation).Schedule(() =>
+ {
+ if (beatIndex % (int)timingPoint.TimeSignature == 0)
+ sampleDownbeat.Play();
+ else
+ sampleBeat.Play();
+ });
+ }
logoBeatContainer
.ScaleTo(1 - 0.02f * amplitudeAdjust, early_activation, Easing.Out).Then()
diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
index 7bc226bb3f..ab7be13479 100644
--- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
@@ -20,17 +20,18 @@ namespace osu.Game.Screens.OnlinePlay
{
protected override bool Stacked => false;
+ protected override bool AllowConfiguration => false;
+
public new Func IsValidMod
{
get => base.IsValidMod;
- set => base.IsValidMod = m => m.HasImplementation && !m.RequiresConfiguration && !(m is ModAutoplay) && value(m);
+ set => base.IsValidMod = m => m.HasImplementation && !(m is ModAutoplay) && value(m);
}
public FreeModSelectOverlay()
{
IsValidMod = m => true;
- CustomiseButton.Alpha = 0;
MultiplierSection.Alpha = 0;
DeselectAllButton.Alpha = 0;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
index 84e8849726..f17d97c3fd 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
@@ -59,6 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
- protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust) && !mod.RequiresConfiguration;
+ protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust);
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 5f2f1366f7..c5130baa94 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -12,6 +12,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
+using osu.Framework.Threading;
+using osu.Game.Configuration;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
@@ -75,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
- Horizontal = 105,
+ Horizontal = HORIZONTAL_OVERFLOW_PADDING + 55,
Vertical = 20
},
Child = new GridContainer
@@ -235,6 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
+ Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING },
Child = userModsSelectOverlay = new UserModSelectOverlay
{
SelectedMods = { BindTarget = UserMods },
@@ -314,12 +317,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
}
+ private ModSettingChangeTracker modSettingChangeTracker;
+ private ScheduledDelegate debouncedModSettingsUpdate;
+
private void onUserModsChanged(ValueChangedEvent> mods)
{
+ modSettingChangeTracker?.Dispose();
+
if (client.Room == null)
return;
client.ChangeUserMods(mods.NewValue);
+
+ modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
+ modSettingChangeTracker.SettingChanged += onModSettingsChanged;
+ }
+
+ private void onModSettingsChanged(Mod mod)
+ {
+ // Debounce changes to mod settings so as to not thrash the network.
+ debouncedModSettingsUpdate?.Cancel();
+ debouncedModSettingsUpdate = Scheduler.AddDelayed(() =>
+ {
+ if (client.Room == null)
+ return;
+
+ client.ChangeUserMods(UserMods.Value);
+ }, 500);
}
private void updateBeatmapAvailability(ValueChangedEvent availability)
@@ -385,14 +409,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client != null)
client.LoadRequested -= onLoadRequested;
+
+ modSettingChangeTracker?.Dispose();
}
private class UserModSelectOverlay : LocalPlayerModSelectOverlay
{
- public UserModSelectOverlay()
- {
- CustomiseButton.Alpha = 0;
- }
}
}
}
diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs
index f1120f55a6..90eebfc05b 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@@ -20,10 +21,6 @@ namespace osu.Game.Screens.Select.Carousel
{
public class CarouselHeader : Container
{
- private SampleChannel sampleHover;
-
- private readonly Box hoverLayer;
-
public Container BorderContainer;
public readonly Bindable State = new Bindable(CarouselItemState.NotSelected);
@@ -44,23 +41,11 @@ namespace osu.Game.Screens.Select.Carousel
Children = new Drawable[]
{
Content,
- hoverLayer = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- Blending = BlendingParameters.Additive,
- },
+ new HoverLayer()
}
};
}
- [BackgroundDependencyLoader]
- private void load(AudioManager audio, OsuColour colours)
- {
- sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}");
- hoverLayer.Colour = colours.Blue.Opacity(0.1f);
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
@@ -97,18 +82,50 @@ namespace osu.Game.Screens.Select.Carousel
}
}
- protected override bool OnHover(HoverEvent e)
+ public class HoverLayer : HoverSampleDebounceComponent
{
- sampleHover?.Play();
+ private SampleChannel sampleHover;
- hoverLayer.FadeIn(100, Easing.OutQuint);
- return base.OnHover(e);
- }
+ private Box box;
- protected override void OnHoverLost(HoverLostEvent e)
- {
- hoverLayer.FadeOut(1000, Easing.OutQuint);
- base.OnHoverLost(e);
+ public HoverLayer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio, OsuColour colours)
+ {
+ InternalChild = box = new Box
+ {
+ Colour = colours.Blue.Opacity(0.1f),
+ Alpha = 0,
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ };
+
+ sampleHover = audio.Samples.Get("SongSelect/song-ping");
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ box.FadeIn(100, Easing.OutQuint);
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ box.FadeOut(1000, Easing.OutQuint);
+ base.OnHoverLost(e);
+ }
+
+ public override void PlayHoverSample()
+ {
+ if (sampleHover == null) return;
+
+ sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2);
+ sampleHover.Play();
+ }
}
}
}
diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs
index 44d908fc46..ab4f3f4796 100644
--- a/osu.Game/Screens/Select/Details/AdvancedStats.cs
+++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs
@@ -18,7 +18,6 @@ using System.Threading;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Configuration;
-using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
namespace osu.Game.Screens.Select.Details
@@ -83,32 +82,22 @@ namespace osu.Game.Screens.Select.Details
mods.BindValueChanged(modsChanged, true);
}
- private readonly List references = new List();
+ private ModSettingChangeTracker modSettingChangeTracker;
+ private ScheduledDelegate debouncedStatisticsUpdate;
private void modsChanged(ValueChangedEvent> mods)
{
- // TODO: find a more permanent solution for this if/when it is needed in other components.
- // this is generating drawables for the only purpose of storing bindable references.
- foreach (var r in references)
- r.Dispose();
+ modSettingChangeTracker?.Dispose();
- references.Clear();
-
- ScheduledDelegate debounce = null;
-
- foreach (var mod in mods.NewValue.OfType())
+ modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
+ modSettingChangeTracker.SettingChanged += m =>
{
- foreach (var setting in mod.CreateSettingsControls().OfType())
- {
- setting.SettingChanged += () =>
- {
- debounce?.Cancel();
- debounce = Scheduler.AddDelayed(updateStatistics, 100);
- };
+ if (!(m is IApplicableToDifficulty))
+ return;
- references.Add(setting);
- }
- }
+ debouncedStatisticsUpdate?.Cancel();
+ debouncedStatisticsUpdate = Scheduler.AddDelayed(updateStatistics, 100);
+ };
updateStatistics();
}
@@ -173,6 +162,7 @@ namespace osu.Game.Screens.Select.Details
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+ modSettingChangeTracker?.Dispose();
starDifficultyCancellationSource?.Cancel();
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index f866b232d8..d68a8a515c 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -30,7 +30,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 22d104f2e1..87ebd41fee 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -71,7 +71,7 @@
-
+