diff --git a/osu.Android.props b/osu.Android.props
index 7060e88026..a522a5f43d 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
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.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/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..332163454a 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; }
@@ -233,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Objects
HeadCircle.Samples = this.GetNodeSamples(0);
}
- public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
+ public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
index f6d46aeef5..5672283230 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
@@ -1,9 +1,19 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.Judgements;
+
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderHeadCircle : HitCircle
{
+ ///
+ /// Whether to treat this as a normal for judgement purposes.
+ /// If false, this will be judged as a instead.
+ ///
+ public bool JudgeAsNormalHitCircle = true;
+
+ public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement();
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
index a427ee1955..725dbe81fb 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
@@ -33,10 +33,5 @@ namespace osu.Game.Rulesets.Osu.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderTickJudgement();
-
- public class SliderTickJudgement : OsuJudgement
- {
- public override HitResult MaxResult => HitResult.LargeTickHit;
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs
index c8fe4f41ca..7314021a14 100644
--- a/osu.Game.Rulesets.Osu/OsuInputManager.cs
+++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu
protected override bool Handle(UIEvent e)
{
- if (e is MouseMoveEvent && !AllowUserCursorMovement) return false;
+ if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false;
return base.Handle(e);
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index cba0c5be14..18324a18a8 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -163,6 +163,7 @@ namespace osu.Game.Rulesets.Osu
{
new OsuModTarget(),
new OsuModDifficultyAdjust(),
+ new OsuModClassic()
};
case ModType.Automation:
diff --git a/osu.Game.Rulesets.Osu/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.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/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index e2c98c0aad..0f7a9b442d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -7,7 +7,10 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
@@ -19,16 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
{
[SetUp]
- public new void Setup() => Schedule(() =>
- {
- Child = new ParticipantsList
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Y,
- Size = new Vector2(380, 0.7f)
- };
- });
+ public new void Setup() => Schedule(createNewParticipantsList);
[Test]
public void TestAddUser()
@@ -75,6 +69,50 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.User == secondUser);
}
+ [Test]
+ public void TestGameStateHasPriorityOverDownloadState()
+ {
+ AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
+ checkProgressBarVisibility(true);
+
+ AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Results));
+ checkProgressBarVisibility(false);
+ AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent);
+
+ AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Idle));
+ checkProgressBarVisibility(true);
+ }
+
+ [Test]
+ public void TestCorrectInitialState()
+ {
+ AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
+ AddStep("recreate list", createNewParticipantsList);
+ checkProgressBarVisibility(true);
+ }
+
+ [Test]
+ public void TestBeatmapDownloadingStates()
+ {
+ AddStep("set to no map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
+ AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
+
+ checkProgressBarVisibility(true);
+
+ AddRepeatStep("increment progress", () =>
+ {
+ var progress = this.ChildrenOfType().Single().User.BeatmapAvailability.DownloadProgress ?? 0;
+ Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f)));
+ }, 25);
+
+ AddAssert("progress bar increased", () => this.ChildrenOfType().Single().Current.Value > 0);
+
+ AddStep("set to importing map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Importing()));
+ checkProgressBarVisibility(false);
+
+ AddStep("set to available", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()));
+ }
+
[Test]
public void TestToggleReadyState()
{
@@ -122,6 +160,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1));
+
+ if (RNG.NextBool())
+ {
+ var beatmapState = (DownloadState)RNG.Next(0, (int)DownloadState.LocallyAvailable + 1);
+
+ switch (beatmapState)
+ {
+ case DownloadState.NotDownloaded:
+ Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.NotDownloaded());
+ break;
+
+ case DownloadState.Downloading:
+ Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Downloading(RNG.NextSingle()));
+ break;
+
+ case DownloadState.Importing:
+ Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Importing());
+ break;
+ }
+ }
}
});
}
@@ -152,5 +210,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep($"set state: {state}", () => Client.ChangeUserState(0, state));
}
}
+
+ private void createNewParticipantsList()
+ {
+ Child = new ParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Size = new Vector2(380, 0.7f) };
+ }
+
+ private void checkProgressBarVisibility(bool visible) =>
+ AddUntilStep($"progress bar {(visible ? "is" : "is not")}visible", () =>
+ this.ChildrenOfType().Single().IsPresent == visible);
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
index 1d13c6229c..7d83ba569d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
@@ -87,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault();
+ SelectedMods.Value = Array.Empty();
});
AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect()));
@@ -143,7 +144,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// Tests that the same instances are not shared between two playlist items.
///
[Test]
- [Ignore("Temporarily disabled due to a non-trivial test failure")]
public void TestNewItemHasNewModInstances()
{
AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
diff --git a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs
index 8f20bcdcc1..dc468bb62d 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
private class TestFullscreenOverlay : FullscreenOverlay
{
public TestFullscreenOverlay()
- : base(OverlayColourScheme.Pink, null)
+ : base(OverlayColourScheme.Pink)
{
Children = new Drawable[]
{
@@ -52,6 +52,17 @@ namespace osu.Game.Tests.Visual.Online
},
};
}
+
+ protected override OverlayHeader CreateHeader() => new TestHeader();
+
+ internal class TestHeader : OverlayHeader
+ {
+ protected override OverlayTitle CreateTitle() => new TestTitle();
+
+ internal class TestTitle : OverlayTitle
+ {
+ }
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs
index 626f545b91..aff510dd95 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Online
Add(rankingsOverlay = new TestRankingsOverlay
{
Country = { BindTarget = countryBindable },
- Scope = { BindTarget = scope },
+ Header = { Current = { BindTarget = scope } },
});
}
@@ -65,8 +65,6 @@ namespace osu.Game.Tests.Visual.Online
private class TestRankingsOverlay : RankingsOverlay
{
public new Bindable Country => base.Country;
-
- public new Bindable Scope => base.Scope;
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 37ebc72984..2885dbee00 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -39,7 +39,11 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[SetUp]
- public void SetUp() => Schedule(() => createDisplay(() => new TestModSelectOverlay()));
+ public void SetUp() => Schedule(() =>
+ {
+ SelectedMods.Value = Array.Empty();
+ createDisplay(() => new TestModSelectOverlay());
+ });
[SetUpSteps]
public void SetUpSteps()
@@ -47,6 +51,24 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("show", () => modSelect.Show());
}
+ [Test]
+ public void TestSettingsResetOnDeselection()
+ {
+ var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } };
+
+ changeRuleset(0);
+
+ AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; });
+
+ AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
+
+ AddStep("deselect", () => modSelect.DeselectAllButton.Click());
+ AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
+
+ AddStep("reselect", () => modSelect.GetModButton(osuModDoubleTime).Click());
+ AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
+ }
+
[Test]
public void TestAnimationFlushOnClose()
{
@@ -152,6 +174,45 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
+ [Test]
+ public void TestSettingsAreRetainedOnReload()
+ {
+ changeRuleset(0);
+
+ AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
+
+ AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
+
+ AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay()));
+
+ AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
+ }
+
+ [Test]
+ public void TestExternallySetModIsReplacedByOverlayInstance()
+ {
+ Mod external = new OsuModDoubleTime();
+ Mod overlayButtonMod = null;
+
+ changeRuleset(0);
+
+ AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
+
+ AddAssert("ensure button is selected", () =>
+ {
+ var button = modSelect.GetModButton(SelectedMods.Value.Single());
+ overlayButtonMod = button.SelectedMod;
+ return overlayButtonMod.GetType() == external.GetType();
+ });
+
+ // Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own
+ AddAssert("mod instance doesn't match", () => external != overlayButtonMod);
+
+ AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1);
+ AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Contains(overlayButtonMod));
+ AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Contains(external));
+ }
+
[Test]
public void TestNonStacked()
{
@@ -313,7 +374,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createDisplay(Func createOverlayFunc)
{
- SelectedMods.Value = Array.Empty();
Children = new Drawable[]
{
modSelect = createOverlayFunc().With(d =>
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
index 43ba23e6c6..d0f6f3fe47 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
@@ -105,6 +105,15 @@ namespace osu.Game.Tests.Visual.UserInterface
checkDisplayedCount(3);
}
+ [Test]
+ public void TestError()
+ {
+ setState(Visibility.Visible);
+ AddStep(@"error #1", sendErrorNotification);
+ AddAssert("Is visible", () => notificationOverlay.State.Value == Visibility.Visible);
+ checkDisplayedCount(1);
+ }
+
[Test]
public void TestSpam()
{
@@ -179,7 +188,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void sendBarrage()
{
- switch (RNG.Next(0, 4))
+ switch (RNG.Next(0, 5))
{
case 0:
sendHelloNotification();
@@ -196,6 +205,10 @@ namespace osu.Game.Tests.Visual.UserInterface
case 3:
sendDownloadProgress();
break;
+
+ case 4:
+ sendErrorNotification();
+ break;
}
}
@@ -214,6 +227,11 @@ namespace osu.Game.Tests.Visual.UserInterface
notificationOverlay.Post(new BackgroundNotification { Text = @"Welcome to osu!. Enjoy your stay!" });
}
+ private void sendErrorNotification()
+ {
+ notificationOverlay.Post(new SimpleErrorNotification { Text = @"Rut roh!. Something went wrong!" });
+ }
+
private void sendManyNotifications()
{
for (int i = 0; i < 10; i++)
diff --git a/osu.Game/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/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
index 41fd37a0d7..ee99a39523 100644
--- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Graphics.Containers
{
private SampleChannel samplePopIn;
private SampleChannel samplePopOut;
+ protected virtual string PopInSampleName => "UI/overlay-pop-in";
+ protected virtual string PopOutSampleName => "UI/overlay-pop-out";
protected override bool BlockNonPositionalInput => true;
@@ -40,8 +42,8 @@ namespace osu.Game.Graphics.Containers
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio)
{
- samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in");
- samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out");
+ samplePopIn = audio.Samples.Get(PopInSampleName);
+ samplePopOut = audio.Samples.Get(PopOutSampleName);
}
protected override void LoadComplete()
diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs
index a1d06711db..fa43d4543f 100644
--- a/osu.Game/Graphics/UserInterface/HoverSounds.cs
+++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
+using osu.Framework.Utils;
namespace osu.Game.Graphics.UserInterface
{
@@ -47,15 +48,20 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
+ if (sampleHover == null)
+ return false;
+
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime;
if (enoughTimePassedSinceLastPlayback)
{
- sampleHover?.Play();
+ sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08);
+ sampleHover.Play();
+
lastPlaybackTime.Value = Time.Current;
}
- return base.OnHover(e);
+ return false;
}
}
@@ -68,6 +74,9 @@ namespace osu.Game.Graphics.UserInterface
Normal,
[Description("-softer")]
- Soft
+ Soft,
+
+ [Description("-toolbar")]
+ Toolbar
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
index f6effa0834..0d00bc0dce 100644
--- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Graphics.UserInterface
RelativeSizeAxes = Axes.X,
},
Nub = new Nub(),
- new HoverClickSounds()
+ new HoverSounds()
};
if (nubOnRight)
diff --git a/osu.Game/Graphics/UserInterface/ProgressBar.cs b/osu.Game/Graphics/UserInterface/ProgressBar.cs
index d271cd121c..50367e600e 100644
--- a/osu.Game/Graphics/UserInterface/ProgressBar.cs
+++ b/osu.Game/Graphics/UserInterface/ProgressBar.cs
@@ -40,8 +40,19 @@ namespace osu.Game.Graphics.UserInterface
set => CurrentNumber.Value = value;
}
- public ProgressBar()
+ private readonly bool allowSeek;
+
+ public override bool HandlePositionalInput => allowSeek;
+ public override bool HandleNonPositionalInput => allowSeek;
+
+ ///
+ /// Construct a new progress bar.
+ ///
+ /// Whether the user should be allowed to click/drag to adjust the value.
+ public ProgressBar(bool allowSeek)
{
+ this.allowSeek = allowSeek;
+
CurrentNumber.MinValue = 0;
CurrentNumber.MaxValue = 1;
RelativeSizeAxes = Axes.X;
diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
index 99e87677fa..dd854acc32 100644
--- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
+++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
@@ -3,6 +3,7 @@
using System.Buffers;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Text;
using MessagePack;
using MessagePack.Formatters;
@@ -41,6 +42,13 @@ namespace osu.Game.Online.API
primitiveFormatter.Serialize(ref writer, b.Value, options);
break;
+ case IBindable u:
+ // A mod with unknown (e.g. enum) generic type.
+ var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value));
+ Debug.Assert(valueMethod != null);
+ primitiveFormatter.Serialize(ref writer, valueMethod.GetValue(u), options);
+ break;
+
default:
// fall back for non-bindable cases.
primitiveFormatter.Serialize(ref writer, kvp.Value, options);
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 493518ac80..9ba8f792a8 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -239,6 +239,7 @@ namespace osu.Game.Online.Multiplayer
newConnection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
newConnection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
newConnection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
+ newConnection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
newConnection.Closed += ex =>
{
diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs
index ad4b3c5151..d6f4c45a75 100644
--- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs
+++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Logging;
+using osu.Framework.Threading;
using osu.Game.Beatmaps;
namespace osu.Game.Online.Rooms
@@ -24,19 +25,33 @@ namespace osu.Game.Online.Rooms
///
public IBindable Availability => availability;
- private readonly Bindable availability = new Bindable();
+ private readonly Bindable availability = new Bindable(BeatmapAvailability.LocallyAvailable());
- public OnlinePlayBeatmapAvailablilityTracker()
- {
- State.BindValueChanged(_ => updateAvailability());
- Progress.BindValueChanged(_ => updateAvailability(), true);
- }
+ private ScheduledDelegate progressUpdate;
protected override void LoadComplete()
{
base.LoadComplete();
- SelectedItem.BindValueChanged(item => Model.Value = item.NewValue?.Beatmap.Value.BeatmapSet, true);
+ SelectedItem.BindValueChanged(item =>
+ {
+ // the underlying playlist is regularly cleared for maintenance purposes (things which probably need to be fixed eventually).
+ // to avoid exposing a state change when there may actually be none, ignore all nulls for now.
+ if (item.NewValue == null)
+ return;
+
+ Model.Value = item.NewValue.Beatmap.Value.BeatmapSet;
+ }, true);
+
+ Progress.BindValueChanged(_ =>
+ {
+ // incoming progress changes are going to be at a very high rate.
+ // we don't want to flood the network with this, so rate limit how often we send progress updates.
+ if (progressUpdate?.Completed != false)
+ progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
+ });
+
+ State.BindValueChanged(_ => updateAvailability(), true);
}
protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet)
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 1a1f7bd233..0dc63dcd4b 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -778,7 +778,7 @@ namespace osu.Game
if (recentLogCount < short_term_display_limit)
{
- Schedule(() => notifications.Post(new SimpleNotification
+ Schedule(() => notifications.Post(new SimpleErrorNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 20d88d33f2..d3936ed27e 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -98,7 +98,14 @@ namespace osu.Game
[Cached(typeof(IBindable))]
protected readonly Bindable Ruleset = new Bindable();
- // todo: move this to SongSelect once Screen has the ability to unsuspend.
+ ///
+ /// The current mod selection for the local user.
+ ///
+ ///
+ /// If a mod select overlay is present, mod instances set to this value are not guaranteed to remain as the provided instance and will be overwritten by a copy.
+ /// In such a case, changes to settings of a mod will *not* propagate after a mod is added to this collection.
+ /// As such, all settings should be finalised before adding a mod to this collection.
+ ///
[Cached]
[Cached(typeof(IBindable>))]
protected readonly Bindable> SelectedMods = new Bindable>(Array.Empty());
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs
index 6a2f2e4569..ca94078401 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
public DownloadProgressBar(BeatmapSetInfo beatmapSet)
: base(beatmapSet)
{
- AddInternal(progressBar = new InteractionDisabledProgressBar
+ AddInternal(progressBar = new ProgressBar(false)
{
Height = 0,
Alpha = 0,
@@ -64,11 +64,5 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
}
}, true);
}
-
- private class InteractionDisabledProgressBar : ProgressBar
- {
- public override bool HandlePositionalInput => false;
- public override bool HandleNonPositionalInput => false;
- }
}
}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 698984b306..5df7a4650e 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -15,98 +15,82 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
-using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.BeatmapListing.Panels;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Overlays
{
- public class BeatmapListingOverlay : FullscreenOverlay
+ public class BeatmapListingOverlay : OnlineOverlay
{
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
private Drawable currentContent;
- private LoadingLayer loadingLayer;
private Container panelTarget;
private FillFlowContainer foundContent;
private NotFoundDrawable notFoundContent;
-
- private OverlayScrollContainer resultScrollContainer;
+ private BeatmapListingFilterControl filterControl;
public BeatmapListingOverlay()
- : base(OverlayColourScheme.Blue, new BeatmapListingHeader())
+ : base(OverlayColourScheme.Blue)
{
}
- private BeatmapListingFilterControl filterControl;
-
[BackgroundDependencyLoader]
private void load()
{
- Children = new Drawable[]
+ Child = new FillFlowContainer
{
- new Box
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background6
- },
- resultScrollContainer = new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new ReverseChildIDFillFlowContainer
+ filterControl = new BeatmapListingFilterControl
+ {
+ TypingStarted = onTypingStarted,
+ SearchStarted = onSearchStarted,
+ SearchFinished = onSearchFinished,
+ },
+ new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- Header,
- filterControl = new BeatmapListingFilterControl
+ new Box
{
- TypingStarted = onTypingStarted,
- SearchStarted = onSearchStarted,
- SearchFinished = onSearchFinished,
+ RelativeSizeAxes = Axes.Both,
+ Colour = ColourProvider.Background4,
},
- new Container
+ panelTarget = new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
+ Padding = new MarginPadding { Horizontal = 20 },
Children = new Drawable[]
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background4,
- },
- panelTarget = new Container
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Padding = new MarginPadding { Horizontal = 20 },
- Children = new Drawable[]
- {
- foundContent = new FillFlowContainer(),
- notFoundContent = new NotFoundDrawable(),
- }
- }
- },
- },
- }
+ foundContent = new FillFlowContainer(),
+ notFoundContent = new NotFoundDrawable(),
+ }
+ }
+ },
},
- },
- loadingLayer = new LoadingLayer(true)
+ }
};
}
+ protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
+
+ protected override Color4 BackgroundColour => ColourProvider.Background6;
+
private void onTypingStarted()
{
// temporary until the textbox/header is updated to always stay on screen.
- resultScrollContainer.ScrollToStart();
+ ScrollFlow.ScrollToStart();
}
protected override void OnFocus(FocusEvent e)
@@ -125,7 +109,7 @@ namespace osu.Game.Overlays
previewTrackManager.StopAnyPlaying(this);
if (panelTarget.Any())
- loadingLayer.Show();
+ Loading.Show();
}
private Task panelLoadDelegate;
@@ -173,7 +157,7 @@ namespace osu.Game.Overlays
private void addContentToPlaceholder(Drawable content)
{
- loadingLayer.Hide();
+ Loading.Hide();
lastFetchDisplayedTime = Time.Current;
if (content == currentContent)
@@ -267,7 +251,7 @@ namespace osu.Game.Overlays
bool shouldShowMore = panelLoadDelegate?.IsCompleted != false
&& Time.Current - lastFetchDisplayedTime > time_between_fetches
- && (resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance));
+ && (ScrollFlow.ScrollableExtent > 0 && ScrollFlow.IsScrolledToEnd(pagination_scroll_distance));
if (shouldShowMore)
filterControl.FetchNextPage();
diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs
index c16ec339bb..bdb3715e73 100644
--- a/osu.Game/Overlays/BeatmapSetOverlay.cs
+++ b/osu.Game/Overlays/BeatmapSetOverlay.cs
@@ -6,20 +6,19 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
-using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Overlays.Comments;
using osu.Game.Rulesets;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Overlays
{
- public class BeatmapSetOverlay : FullscreenOverlay
+ public class BeatmapSetOverlay : OnlineOverlay
{
public const float X_PADDING = 40;
public const float Y_PADDING = 25;
@@ -33,55 +32,27 @@ namespace osu.Game.Overlays
// receive input outside our bounds so we can trigger a close event on ourselves.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
- private readonly Box background;
-
public BeatmapSetOverlay()
- : base(OverlayColourScheme.Blue, new BeatmapSetHeader())
+ : base(OverlayColourScheme.Blue)
{
- OverlayScrollContainer scroll;
Info info;
CommentsSection comments;
- Children = new Drawable[]
+ Child = new FillFlowContainer
{
- background = new Box
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 20),
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both
- },
- scroll = new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new ReverseChildIDFillFlowContainer
+ info = new Info(),
+ new ScoresContainer
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 20),
- Children = new[]
- {
- new BeatmapSetLayoutSection
- {
- Child = new ReverseChildIDFillFlowContainer
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- Header,
- info = new Info()
- }
- },
- },
- new ScoresContainer
- {
- Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap }
- },
- comments = new CommentsSection()
- },
+ Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap }
},
- },
+ comments = new CommentsSection()
+ }
};
Header.BeatmapSet.BindTo(beatmapSet);
@@ -91,16 +62,13 @@ namespace osu.Game.Overlays
Header.HeaderContent.Picker.Beatmap.ValueChanged += b =>
{
info.Beatmap = b.NewValue;
-
- scroll.ScrollToStart();
+ ScrollFlow.ScrollToStart();
};
}
- [BackgroundDependencyLoader]
- private void load()
- {
- background.Colour = ColourProvider.Background6;
- }
+ protected override BeatmapSetHeader CreateHeader() => new BeatmapSetHeader();
+
+ protected override Color4 BackgroundColour => ColourProvider.Background6;
protected override void PopOutComplete()
{
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index c7e9a86fa4..05bad30107 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -11,22 +11,18 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Changelog;
+using osuTK.Graphics;
namespace osu.Game.Overlays
{
- public class ChangelogOverlay : FullscreenOverlay
+ public class ChangelogOverlay : OnlineOverlay
{
public readonly Bindable Current = new Bindable();
- private Container content;
-
private SampleChannel sampleBack;
private List builds;
@@ -34,45 +30,14 @@ namespace osu.Game.Overlays
protected List Streams;
public ChangelogOverlay()
- : base(OverlayColourScheme.Purple, new ChangelogHeader())
+ : base(OverlayColourScheme.Purple)
{
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background4,
- },
- new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new ReverseChildIDFillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- Header.With(h =>
- {
- h.ListingSelected = ShowListing;
- h.Build.BindTarget = Current;
- }),
- content = new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- }
- },
- },
- },
- };
+ Header.Build.BindTarget = Current;
sampleBack = audio.Samples.Get(@"UI/generic-select-soft");
@@ -85,6 +50,13 @@ namespace osu.Game.Overlays
});
}
+ protected override ChangelogHeader CreateHeader() => new ChangelogHeader
+ {
+ ListingSelected = ShowListing,
+ };
+
+ protected override Color4 BackgroundColour => ColourProvider.Background4;
+
public void ShowListing()
{
Current.Value = null;
@@ -198,16 +170,16 @@ namespace osu.Game.Overlays
private void loadContent(ChangelogContent newContent)
{
- content.FadeTo(0.2f, 300, Easing.OutQuint);
+ Content.FadeTo(0.2f, 300, Easing.OutQuint);
loadContentCancellation?.Cancel();
LoadComponentAsync(newContent, c =>
{
- content.FadeIn(300, Easing.OutQuint);
+ Content.FadeIn(300, Easing.OutQuint);
c.BuildSelected = ShowBuild;
- content.Child = c;
+ Child = c;
}, (loadContentCancellation = new CancellationTokenSource()).Token);
}
}
diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs
index 03c320debe..83ad8faf1c 100644
--- a/osu.Game/Overlays/DashboardOverlay.cs
+++ b/osu.Game/Overlays/DashboardOverlay.cs
@@ -2,155 +2,35 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Threading;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.API;
using osu.Game.Overlays.Dashboard;
using osu.Game.Overlays.Dashboard.Friends;
namespace osu.Game.Overlays
{
- public class DashboardOverlay : FullscreenOverlay
+ public class DashboardOverlay : TabbableOnlineOverlay
{
- private CancellationTokenSource cancellationToken;
-
- private Container content;
- private LoadingLayer loading;
- private OverlayScrollContainer scrollFlow;
-
public DashboardOverlay()
- : base(OverlayColourScheme.Purple, new DashboardOverlayHeader
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Depth = -float.MaxValue
- })
+ : base(OverlayColourScheme.Purple)
{
}
- private readonly IBindable apiState = new Bindable();
+ protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader();
- [BackgroundDependencyLoader]
- private void load(IAPIProvider api)
+ protected override void CreateDisplayToLoad(DashboardOverlayTabs tab)
{
- apiState.BindTo(api.State);
- apiState.BindValueChanged(onlineStateChanged, true);
-
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background5
- },
- scrollFlow = new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- Header,
- content = new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y
- }
- }
- }
- },
- loading = new LoadingLayer(true),
- };
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- Header.Current.BindValueChanged(onTabChanged);
- }
-
- private bool displayUpdateRequired = true;
-
- protected override void PopIn()
- {
- base.PopIn();
-
- // We don't want to create a new display on every call, only when exiting from fully closed state.
- if (displayUpdateRequired)
- {
- Header.Current.TriggerChange();
- displayUpdateRequired = false;
- }
- }
-
- protected override void PopOutComplete()
- {
- base.PopOutComplete();
- loadDisplay(Empty());
- displayUpdateRequired = true;
- }
-
- private void loadDisplay(Drawable display)
- {
- scrollFlow.ScrollToStart();
-
- LoadComponentAsync(display, loaded =>
- {
- if (API.IsLoggedIn)
- loading.Hide();
-
- content.Child = loaded;
- }, (cancellationToken = new CancellationTokenSource()).Token);
- }
-
- private void onTabChanged(ValueChangedEvent tab)
- {
- cancellationToken?.Cancel();
- loading.Show();
-
- if (!API.IsLoggedIn)
- {
- loadDisplay(Empty());
- return;
- }
-
- switch (tab.NewValue)
+ switch (tab)
{
case DashboardOverlayTabs.Friends:
- loadDisplay(new FriendDisplay());
+ LoadDisplay(new FriendDisplay());
break;
case DashboardOverlayTabs.CurrentlyPlaying:
- loadDisplay(new CurrentlyPlayingDisplay());
+ LoadDisplay(new CurrentlyPlayingDisplay());
break;
default:
- throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented");
+ throw new NotImplementedException($"Display for {tab} tab is not implemented");
}
}
-
- private void onlineStateChanged(ValueChangedEvent state) => Schedule(() =>
- {
- if (State.Value == Visibility.Hidden)
- return;
-
- Header.Current.TriggerChange();
- });
-
- protected override void Dispose(bool isDisposing)
- {
- cancellationToken?.Cancel();
- base.Dispose(isDisposing);
- }
}
}
diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs
index 9f9dbdbaf1..4cc17a4c14 100644
--- a/osu.Game/Overlays/DialogOverlay.cs
+++ b/osu.Game/Overlays/DialogOverlay.cs
@@ -14,6 +14,9 @@ namespace osu.Game.Overlays
{
private readonly Container dialogContainer;
+ protected override string PopInSampleName => "UI/dialog-pop-in";
+ protected override string PopOutSampleName => "UI/dialog-pop-out";
+
public PopupDialog CurrentDialog { get; private set; }
public DialogOverlay()
diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs
index 6f56d95929..735f0bcbd4 100644
--- a/osu.Game/Overlays/FullscreenOverlay.cs
+++ b/osu.Game/Overlays/FullscreenOverlay.cs
@@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osuTK.Graphics;
@@ -15,21 +17,27 @@ namespace osu.Game.Overlays
public abstract class FullscreenOverlay : WaveOverlayContainer, INamedOverlayComponent
where T : OverlayHeader
{
- public virtual string IconTexture => Header?.Title.IconTexture ?? string.Empty;
- public virtual string Title => Header?.Title.Title ?? string.Empty;
- public virtual string Description => Header?.Title.Description ?? string.Empty;
+ public virtual string IconTexture => Header.Title.IconTexture ?? string.Empty;
+ public virtual string Title => Header.Title.Title ?? string.Empty;
+ public virtual string Description => Header.Title.Description ?? string.Empty;
public T Header { get; }
+ protected virtual Color4 BackgroundColour => ColourProvider.Background5;
+
[Resolved]
protected IAPIProvider API { get; private set; }
[Cached]
protected readonly OverlayColourProvider ColourProvider;
- protected FullscreenOverlay(OverlayColourScheme colourScheme, T header)
+ protected override Container Content => content;
+
+ private readonly Container content;
+
+ protected FullscreenOverlay(OverlayColourScheme colourScheme)
{
- Header = header;
+ Header = CreateHeader();
ColourProvider = new OverlayColourProvider(colourScheme);
@@ -47,6 +55,19 @@ namespace osu.Game.Overlays
Type = EdgeEffectType.Shadow,
Radius = 10
};
+
+ base.Content.AddRange(new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = BackgroundColour
+ },
+ content = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ });
}
[BackgroundDependencyLoader]
@@ -58,6 +79,9 @@ namespace osu.Game.Overlays
Waves.FourthWaveColour = ColourProvider.Dark3;
}
+ [NotNull]
+ protected abstract T CreateHeader();
+
public override void Show()
{
if (State.Value == Visibility.Visible)
diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs
index 8e0d1f5bbd..5e3733cd5e 100644
--- a/osu.Game/Overlays/Mods/ModButton.cs
+++ b/osu.Game/Overlays/Mods/ModButton.cs
@@ -46,8 +46,9 @@ namespace osu.Game.Overlays.Mods
/// Change the selected mod index of this button.
///
/// The new index.
+ /// Whether any settings applied to the mod should be reset on selection.
/// Whether the selection changed.
- private bool changeSelectedIndex(int newIndex)
+ private bool changeSelectedIndex(int newIndex, bool resetSettings = true)
{
if (newIndex == selectedIndex) return false;
@@ -69,6 +70,9 @@ namespace osu.Game.Overlays.Mods
Mod newSelection = SelectedMod ?? Mods[0];
+ if (resetSettings)
+ newSelection.ResetSettingsToDefaults();
+
Schedule(() =>
{
if (beforeSelected != Selected)
@@ -209,11 +213,17 @@ namespace osu.Game.Overlays.Mods
Deselect();
}
- public bool SelectAt(int index)
+ ///
+ /// Select the mod at the provided index.
+ ///
+ /// The index to select.
+ /// Whether any settings applied to the mod should be reset on selection.
+ /// Whether the selection changed.
+ public bool SelectAt(int index, bool resetSettings = true)
{
if (!Mods[index].HasImplementation) return false;
- changeSelectedIndex(index);
+ changeSelectedIndex(index, resetSettings);
return true;
}
diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs
index ecbcba7ad3..c3e56abd05 100644
--- a/osu.Game/Overlays/Mods/ModSection.cs
+++ b/osu.Game/Overlays/Mods/ModSection.cs
@@ -197,8 +197,11 @@ namespace osu.Game.Overlays.Mods
continue;
var buttonMod = button.Mods[index];
+
+ // as this is likely coming from an external change, ensure the settings of the mod are in sync.
buttonMod.CopyFrom(mod);
- button.SelectAt(index);
+
+ button.SelectAt(index, false);
return;
}
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 93fe693937..a713e90f6c 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -51,6 +51,11 @@ namespace osu.Game.Overlays.Mods
///
protected virtual bool Stacked => true;
+ ///
+ /// Whether configurable s can be configured by the local user.
+ ///
+ protected virtual bool AllowConfiguration => true;
+
[NotNull]
private Func isValidMod = m => true;
@@ -300,6 +305,7 @@ namespace osu.Game.Overlays.Mods
Text = "Customisation",
Action = () => ModSettingsContainer.ToggleVisibility(),
Enabled = { Value = false },
+ Alpha = AllowConfiguration ? 1 : 0,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
@@ -372,7 +378,10 @@ namespace osu.Game.Overlays.Mods
base.LoadComplete();
availableMods.BindValueChanged(_ => updateAvailableMods(), true);
- SelectedMods.BindValueChanged(_ => updateSelectedButtons(), true);
+
+ // intentionally bound after the above line to avoid a potential update feedback cycle.
+ // i haven't actually observed this happening but as updateAvailableMods() changes the selection it is plausible.
+ SelectedMods.BindValueChanged(_ => updateSelectedButtons());
}
protected override void PopOut()
@@ -479,10 +488,10 @@ namespace osu.Game.Overlays.Mods
foreach (var section in ModSectionsContainer.Children)
section.UpdateSelectedButtons(selectedMods);
- updateMods();
+ updateMultiplier();
}
- private void updateMods()
+ private void updateMultiplier()
{
var multiplier = 1.0;
@@ -509,7 +518,8 @@ namespace osu.Game.Overlays.Mods
OnModSelected(selectedMod);
- if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show();
+ if (selectedMod.RequiresConfiguration && AllowConfiguration)
+ ModSettingsContainer.Show();
}
else
{
diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs
index 5820d405d4..08e8331dd3 100644
--- a/osu.Game/Overlays/NewsOverlay.cs
+++ b/osu.Game/Overlays/NewsOverlay.cs
@@ -2,67 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using System.Threading;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.News;
using osu.Game.Overlays.News.Displays;
namespace osu.Game.Overlays
{
- public class NewsOverlay : FullscreenOverlay
+ public class NewsOverlay : OnlineOverlay
{
private readonly Bindable article = new Bindable(null);
- private Container content;
- private LoadingLayer loading;
- private OverlayScrollContainer scrollFlow;
-
public NewsOverlay()
- : base(OverlayColourScheme.Purple, new NewsHeader())
+ : base(OverlayColourScheme.Purple)
{
}
- [BackgroundDependencyLoader]
- private void load()
- {
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background5,
- },
- scrollFlow = new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- Header.With(h =>
- {
- h.ShowFrontPage = ShowFrontPage;
- }),
- content = new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- }
- },
- },
- },
- loading = new LoadingLayer(true),
- };
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
@@ -71,6 +26,11 @@ namespace osu.Game.Overlays
article.BindValueChanged(onArticleChanged);
}
+ protected override NewsHeader CreateHeader() => new NewsHeader
+ {
+ ShowFrontPage = ShowFrontPage
+ };
+
private bool displayUpdateRequired = true;
protected override void PopIn()
@@ -107,7 +67,7 @@ namespace osu.Game.Overlays
private void onArticleChanged(ValueChangedEvent e)
{
cancellationToken?.Cancel();
- loading.Show();
+ Loading.Show();
if (e.NewValue == null)
{
@@ -122,11 +82,11 @@ namespace osu.Game.Overlays
protected void LoadDisplay(Drawable display)
{
- scrollFlow.ScrollToStart();
+ ScrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
- content.Child = loaded;
- loading.Hide();
+ Child = loaded;
+ Loading.Hide();
}, (cancellationToken = new CancellationTokenSource()).Token);
}
diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs
index 2dc6b39a92..daf931bc24 100644
--- a/osu.Game/Overlays/Notifications/Notification.cs
+++ b/osu.Game/Overlays/Notifications/Notification.cs
@@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -40,6 +42,11 @@ namespace osu.Game.Overlays.Notifications
///
public virtual bool DisplayOnTop => true;
+ private 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 9beb859f28..2866d2ad6d 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -51,6 +51,9 @@ namespace osu.Game.Overlays
private Container dragContainer;
private Container playerContainer;
+ protected override string PopInSampleName => "UI/now-playing-pop-in";
+ protected override string PopOutSampleName => "UI/now-playing-pop-out";
+
///
/// Provide a source for the toolbar height.
///
@@ -84,11 +87,6 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
- playlist = new PlaylistOverlay
- {
- RelativeSizeAxes = Axes.X,
- Y = player_height + 10,
- },
playerContainer = new Container
{
RelativeSizeAxes = Axes.X,
@@ -171,7 +169,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.CentreRight,
Position = new Vector2(-bottom_black_area_height / 2, 0),
Icon = FontAwesome.Solid.Bars,
- Action = () => playlist.ToggleVisibility(),
+ Action = togglePlaylist
},
}
},
@@ -191,13 +189,35 @@ namespace osu.Game.Overlays
};
}
+ private void togglePlaylist()
+ {
+ if (playlist == null)
+ {
+ LoadComponentAsync(playlist = new PlaylistOverlay
+ {
+ RelativeSizeAxes = Axes.X,
+ Y = player_height + 10,
+ }, _ =>
+ {
+ dragContainer.Add(playlist);
+
+ playlist.BeatmapSets.BindTo(musicController.BeatmapSets);
+ playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
+
+ togglePlaylist();
+ });
+
+ return;
+ }
+
+ if (!beatmap.Disabled)
+ playlist.ToggleVisibility();
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
- playlist.BeatmapSets.BindTo(musicController.BeatmapSets);
- playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
-
beatmap.BindDisabledChanged(beatmapDisabledChanged, true);
musicController.TrackChanged += trackChanged;
@@ -306,7 +326,7 @@ namespace osu.Game.Overlays
private void beatmapDisabledChanged(bool disabled)
{
if (disabled)
- playlist.Hide();
+ playlist?.Hide();
prevButton.Enabled.Value = !disabled;
nextButton.Enabled.Value = !disabled;
@@ -411,6 +431,11 @@ namespace osu.Game.Overlays
private class HoverableProgressBar : ProgressBar
{
+ public HoverableProgressBar()
+ : base(true)
+ {
+ }
+
protected override bool OnHover(HoverEvent e)
{
this.ResizeHeightTo(progress_height, 500, Easing.OutQuint);
diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs
index 8e8a99a0a7..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/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs
new file mode 100644
index 0000000000..7c9f751d3b
--- /dev/null
+++ b/osu.Game/Overlays/OnlineOverlay.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Overlays
+{
+ public abstract class OnlineOverlay : FullscreenOverlay
+ where T : OverlayHeader
+ {
+ protected override Container Content => content;
+
+ protected readonly OverlayScrollContainer ScrollFlow;
+ protected readonly LoadingLayer Loading;
+ private readonly Container content;
+
+ protected OnlineOverlay(OverlayColourScheme colourScheme)
+ : base(colourScheme)
+ {
+ base.Content.AddRange(new Drawable[]
+ {
+ ScrollFlow = new OverlayScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ScrollbarVisible = false,
+ Child = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ Header.With(h => h.Depth = float.MinValue),
+ content = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }
+ }
+ }
+ },
+ Loading = new LoadingLayer(true)
+ });
+ }
+ }
+}
diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs
index 25350e310a..a093969115 100644
--- a/osu.Game/Overlays/RankingsOverlay.cs
+++ b/osu.Game/Overlays/RankingsOverlay.cs
@@ -4,96 +4,32 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays.Rankings;
using osu.Game.Users;
using osu.Game.Rulesets;
using osu.Game.Online.API;
-using System.Threading;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Rankings.Tables;
namespace osu.Game.Overlays
{
- public class RankingsOverlay : FullscreenOverlay
+ public class RankingsOverlay : TabbableOnlineOverlay
{
protected Bindable Country => Header.Country;
- protected Bindable Scope => Header.Current;
-
- private readonly OverlayScrollContainer scrollFlow;
- private readonly Container contentContainer;
- private readonly LoadingLayer loading;
- private readonly Box background;
-
private APIRequest lastRequest;
- private CancellationTokenSource cancellationToken;
[Resolved]
private IAPIProvider api { get; set; }
- public RankingsOverlay()
- : base(OverlayColourScheme.Green, new RankingsOverlayHeader
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Depth = -float.MaxValue
- })
- {
- loading = new LoadingLayer(true);
-
- Children = new Drawable[]
- {
- background = new Box
- {
- RelativeSizeAxes = Axes.Both
- },
- scrollFlow = new OverlayScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- ScrollbarVisible = false,
- Child = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- Header,
- new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Children = new Drawable[]
- {
- contentContainer = new Container
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AutoSizeAxes = Axes.Y,
- RelativeSizeAxes = Axes.X,
- Margin = new MarginPadding { Bottom = 10 }
- },
- }
- }
- }
- }
- },
- loading
- };
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- background.Colour = ColourProvider.Background5;
- }
-
[Resolved]
private Bindable ruleset { get; set; }
+ public RankingsOverlay()
+ : base(OverlayColourScheme.Green)
+ {
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -104,31 +40,33 @@ namespace osu.Game.Overlays
{
// if a country is requested, force performance scope.
if (Country.Value != null)
- Scope.Value = RankingsScope.Performance;
+ Header.Current.Value = RankingsScope.Performance;
- Scheduler.AddOnce(loadNewContent);
- });
-
- Scope.BindValueChanged(_ =>
- {
- // country filtering is only valid for performance scope.
- if (Scope.Value != RankingsScope.Performance)
- Country.Value = null;
-
- Scheduler.AddOnce(loadNewContent);
+ Scheduler.AddOnce(triggerTabChanged);
});
ruleset.BindValueChanged(_ =>
{
- if (Scope.Value == RankingsScope.Spotlights)
+ if (Header.Current.Value == RankingsScope.Spotlights)
return;
- Scheduler.AddOnce(loadNewContent);
+ Scheduler.AddOnce(triggerTabChanged);
});
-
- Scheduler.AddOnce(loadNewContent);
}
+ protected override void OnTabChanged(RankingsScope tab)
+ {
+ // country filtering is only valid for performance scope.
+ if (Header.Current.Value != RankingsScope.Performance)
+ Country.Value = null;
+
+ Scheduler.AddOnce(triggerTabChanged);
+ }
+
+ private void triggerTabChanged() => base.OnTabChanged(Header.Current.Value);
+
+ protected override RankingsOverlayHeader CreateHeader() => new RankingsOverlayHeader();
+
public void ShowCountry(Country requested)
{
if (requested == null)
@@ -139,22 +77,13 @@ namespace osu.Game.Overlays
Country.Value = requested;
}
- public void ShowSpotlights()
+ protected override void CreateDisplayToLoad(RankingsScope tab)
{
- Scope.Value = RankingsScope.Spotlights;
- Show();
- }
-
- private void loadNewContent()
- {
- loading.Show();
-
- cancellationToken?.Cancel();
lastRequest?.Cancel();
- if (Scope.Value == RankingsScope.Spotlights)
+ if (Header.Current.Value == RankingsScope.Spotlights)
{
- loadContent(new SpotlightsLayout
+ LoadDisplay(new SpotlightsLayout
{
Ruleset = { BindTarget = ruleset }
});
@@ -166,19 +95,19 @@ namespace osu.Game.Overlays
if (request == null)
{
- loadContent(null);
+ LoadDisplay(Empty());
return;
}
- request.Success += () => Schedule(() => loadContent(createTableFromResponse(request)));
- request.Failure += _ => Schedule(() => loadContent(null));
+ request.Success += () => Schedule(() => LoadDisplay(createTableFromResponse(request)));
+ request.Failure += _ => Schedule(() => LoadDisplay(Empty()));
api.Queue(request);
}
private APIRequest createScopedRequest()
{
- switch (Scope.Value)
+ switch (Header.Current.Value)
{
case RankingsScope.Performance:
return new GetUserRankingsRequest(ruleset.Value, country: Country.Value?.FlagName);
@@ -216,29 +145,9 @@ namespace osu.Game.Overlays
return null;
}
- private void loadContent(Drawable content)
- {
- scrollFlow.ScrollToStart();
-
- if (content == null)
- {
- contentContainer.Clear();
- loading.Hide();
- return;
- }
-
- LoadComponentAsync(content, loaded =>
- {
- loading.Hide();
- contentContainer.Child = loaded;
- }, (cancellationToken = new CancellationTokenSource()).Token);
- }
-
protected override void Dispose(bool isDisposing)
{
lastRequest?.Cancel();
- cancellationToken?.Cancel();
-
base.Dispose(isDisposing);
}
}
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index 7a5a586f67..f1270f750e 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -40,6 +40,8 @@ namespace osu.Game.Overlays
private SeekLimitedSearchTextBox searchTextBox;
+ protected override string PopInSampleName => "UI/settings-pop-in";
+
///
/// Provide a source for the toolbar height.
///
diff --git a/osu.Game/Overlays/TabbableOnlineOverlay.cs b/osu.Game/Overlays/TabbableOnlineOverlay.cs
new file mode 100644
index 0000000000..8172e99c1b
--- /dev/null
+++ b/osu.Game/Overlays/TabbableOnlineOverlay.cs
@@ -0,0 +1,101 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
+
+namespace osu.Game.Overlays
+{
+ public abstract class TabbableOnlineOverlay : OnlineOverlay
+ where THeader : TabControlOverlayHeader
+ {
+ private readonly IBindable apiState = new Bindable();
+
+ private CancellationTokenSource cancellationToken;
+ private bool displayUpdateRequired = true;
+
+ protected TabbableOnlineOverlay(OverlayColourScheme colourScheme)
+ : base(colourScheme)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IAPIProvider api)
+ {
+ apiState.BindTo(api.State);
+ apiState.BindValueChanged(onlineStateChanged, true);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Header.Current.BindValueChanged(tab => OnTabChanged(tab.NewValue));
+ }
+
+ protected override void PopIn()
+ {
+ base.PopIn();
+
+ // We don't want to create a new display on every call, only when exiting from fully closed state.
+ if (displayUpdateRequired)
+ {
+ Header.Current.TriggerChange();
+ displayUpdateRequired = false;
+ }
+ }
+
+ protected override void PopOutComplete()
+ {
+ base.PopOutComplete();
+ LoadDisplay(Empty());
+ displayUpdateRequired = true;
+ }
+
+ protected void LoadDisplay(Drawable display)
+ {
+ ScrollFlow.ScrollToStart();
+
+ LoadComponentAsync(display, loaded =>
+ {
+ if (API.IsLoggedIn)
+ Loading.Hide();
+
+ Child = loaded;
+ }, (cancellationToken = new CancellationTokenSource()).Token);
+ }
+
+ protected virtual void OnTabChanged(TEnum tab)
+ {
+ cancellationToken?.Cancel();
+ Loading.Show();
+
+ if (!API.IsLoggedIn)
+ {
+ LoadDisplay(Empty());
+ return;
+ }
+
+ CreateDisplayToLoad(tab);
+ }
+
+ protected abstract void CreateDisplayToLoad(TEnum tab);
+
+ private void onlineStateChanged(ValueChangedEvent state) => Schedule(() =>
+ {
+ if (State.Value == Visibility.Hidden)
+ return;
+
+ Header.Current.TriggerChange();
+ });
+
+ protected override void Dispose(bool isDisposing)
+ {
+ cancellationToken?.Cancel();
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
index 49b9c62d85..83f2bdf6cb 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Toolbar
private KeyBindingStore keyBindings { get; set; }
protected ToolbarButton()
- : base(HoverSampleSet.Loud)
+ : base(HoverSampleSet.Toolbar)
{
Width = Toolbar.HEIGHT;
RelativeSizeAxes = Axes.Y;
diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index 7f29545c2e..299a14b250 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -15,6 +15,7 @@ using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Sections;
using osu.Game.Users;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Overlays
{
@@ -29,10 +30,14 @@ namespace osu.Game.Overlays
public const float CONTENT_X_MARGIN = 70;
public UserProfileOverlay()
- : base(OverlayColourScheme.Pink, new ProfileHeader())
+ : base(OverlayColourScheme.Pink)
{
}
+ protected override ProfileHeader CreateHeader() => new ProfileHeader();
+
+ protected override Color4 BackgroundColour => ColourProvider.Background6;
+
public void ShowUser(int userId) => ShowUser(new User { Id = userId });
public void ShowUser(User user, bool fetchOnline = true)
@@ -72,12 +77,6 @@ namespace osu.Game.Overlays
Origin = Anchor.TopCentre,
};
- Add(new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourProvider.Background6
- });
-
Add(sectionsContainer = new ProfileSectionsContainer
{
ExpandableHeader = Header,
diff --git a/osu.Game/Overlays/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/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index 3a8717e678..2a11c92223 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mods
}
///
- /// Copies mod setting values from into this instance.
+ /// Copies mod setting values from into this instance, overwriting all existing settings.
///
/// The mod to copy properties from.
public void CopyFrom(Mod source)
@@ -147,9 +147,7 @@ namespace osu.Game.Rulesets.Mods
var targetBindable = (IBindable)prop.GetValue(this);
var sourceBindable = (IBindable)prop.GetValue(source);
- // we only care about changes that have been made away from defaults.
- if (!sourceBindable.IsDefault)
- CopyAdjustedSetting(targetBindable, sourceBindable);
+ CopyAdjustedSetting(targetBindable, sourceBindable);
}
}
@@ -175,5 +173,10 @@ namespace osu.Game.Rulesets.Mods
}
public bool Equals(IMod other) => GetType() == other?.GetType();
+
+ ///
+ /// Reset all custom settings for this mod back to their defaults.
+ ///
+ public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType()));
}
}
diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
index a531e885db..b70eee4e1d 100644
--- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mods
protected const int LAST_SETTING_ORDER = 2;
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)]
- public BindableNumber DrainRate { get; } = new BindableFloat
+ public BindableNumber DrainRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mods
};
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)]
- public BindableNumber OverallDifficulty { get; } = new BindableFloat
+ public BindableNumber OverallDifficulty { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@@ -53,6 +53,24 @@ namespace osu.Game.Rulesets.Mods
Value = 5,
};
+ [SettingSource("Extended Limits", "Adjust difficulty beyond sane limits.")]
+ public BindableBool ExtendedLimits { get; } = new BindableBool();
+
+ protected ModDifficultyAdjust()
+ {
+ ExtendedLimits.BindValueChanged(extend => ApplyLimits(extend.NewValue));
+ }
+
+ ///
+ /// Changes the difficulty adjustment limits. Occurs when the value of is changed.
+ ///
+ /// Whether limits should extend beyond sane ranges.
+ protected virtual void ApplyLimits(bool extended)
+ {
+ DrainRate.MaxValue = extended ? 11 : 10;
+ OverallDifficulty.MaxValue = extended ? 11 : 10;
+ }
+
public override string SettingDescription
{
get
@@ -141,5 +159,73 @@ namespace osu.Game.Rulesets.Mods
ApplySetting(DrainRate, dr => difficulty.DrainRate = dr);
ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od);
}
+
+ public override void ResetSettingsToDefaults()
+ {
+ base.ResetSettingsToDefaults();
+
+ if (difficulty != null)
+ {
+ // base implementation potentially overwrite modified defaults that came from a beatmap selection.
+ TransferSettings(difficulty);
+ }
+ }
+
+ ///
+ /// A that extends its min/max values to support any assigned value.
+ ///
+ protected class BindableDoubleWithLimitExtension : BindableDouble
+ {
+ public override double Value
+ {
+ get => base.Value;
+ set
+ {
+ if (value < MinValue)
+ MinValue = value;
+ if (value > MaxValue)
+ MaxValue = value;
+ base.Value = value;
+ }
+ }
+ }
+
+ ///
+ /// A that extends its min/max values to support any assigned value.
+ ///
+ protected class BindableFloatWithLimitExtension : BindableFloat
+ {
+ public override float Value
+ {
+ get => base.Value;
+ set
+ {
+ if (value < MinValue)
+ MinValue = value;
+ if (value > MaxValue)
+ MaxValue = value;
+ base.Value = value;
+ }
+ }
+ }
+
+ ///
+ /// A that extends its min/max values to support any assigned value.
+ ///
+ protected class BindableIntWithLimitExtension : BindableInt
+ {
+ public override int Value
+ {
+ get => base.Value;
+ set
+ {
+ if (value < MinValue)
+ MinValue = value;
+ if (value > MaxValue)
+ MaxValue = value;
+ base.Value = value;
+ }
+ }
+ }
}
}
diff --git a/osu.Game/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/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/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
index 54d6ffe38a..f3972ab7f9 100644
--- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
@@ -49,6 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
[Cached]
protected OnlinePlayBeatmapAvailablilityTracker BeatmapAvailablilityTracker { get; }
+ protected IBindable BeatmapAvailability => BeatmapAvailablilityTracker.Availability;
+
protected RoomSubScreen()
{
AddInternal(BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker
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 f030879625..b7adb71e2f 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;
@@ -267,6 +269,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true);
+ BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
UserMods.BindValueChanged(onUserModsChanged);
client.LoadRequested += onLoadRequested;
@@ -313,12 +316,46 @@ 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)
+ {
+ if (client.Room == null)
+ return;
+
+ client.ChangeBeatmapAvailability(availability.NewValue);
+
+ // while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap.
+ if (availability.NewValue != Online.Rooms.BeatmapAvailability.LocallyAvailable()
+ && client.LocalUser?.State == MultiplayerUserState.Ready)
+ client.ChangeState(MultiplayerUserState.Idle);
}
private void onReadyClick()
@@ -371,14 +408,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/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
index 0ee1b6d684..ae32295676 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
@@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
- userStateDisplay.Status = User.State;
+ userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs
index 8d2879fc93..c571b51c83 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -8,83 +10,94 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
public class StateDisplay : CompositeDrawable
{
+ private const double fade_time = 50;
+
+ private SpriteIcon icon;
+ private OsuSpriteText text;
+ private ProgressBar progressBar;
+
public StateDisplay()
{
AutoSizeAxes = Axes.Both;
Alpha = 0;
}
- private MultiplayerUserState status;
-
- private OsuSpriteText text;
- private SpriteIcon icon;
-
- private const double fade_time = 50;
-
- public MultiplayerUserState Status
- {
- set
- {
- if (value == status)
- return;
-
- status = value;
-
- if (IsLoaded)
- updateStatus();
- }
- }
-
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuColour colours)
{
+ this.colours = colours;
+
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
Spacing = new Vector2(5),
Children = new Drawable[]
{
- text = new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12),
- Colour = Color4Extensions.FromHex("#DDFFFF")
- },
icon = new SpriteIcon
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
Icon = FontAwesome.Solid.CheckCircle,
Size = new Vector2(12),
- }
+ },
+ new CircularContainer
+ {
+ Masking = true,
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Children = new Drawable[]
+ {
+ progressBar = new ProgressBar(false)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ BackgroundColour = Color4.Black.Opacity(0.4f),
+ FillColour = colours.Blue,
+ Alpha = 0f,
+ },
+ text = new OsuSpriteText
+ {
+ Padding = new MarginPadding { Horizontal = 5f, Vertical = 1f },
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12),
+ Colour = Color4Extensions.FromHex("#DDFFFF")
+ },
+ }
+ },
}
};
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
- updateStatus();
- }
+ private OsuColour colours;
- [Resolved]
- private OsuColour colours { get; set; }
-
- private void updateStatus()
+ public void UpdateStatus(MultiplayerUserState state, BeatmapAvailability availability)
{
- switch (status)
+ // the only case where the progress bar is used does its own local fade in.
+ // starting by fading out is a sane default.
+ progressBar.FadeOut(fade_time);
+ this.FadeIn(fade_time);
+
+ switch (state)
{
- default:
- this.FadeOut(fade_time);
- return;
+ case MultiplayerUserState.Idle:
+ showBeatmapAvailability(availability);
+ break;
case MultiplayerUserState.Ready:
text.Text = "ready";
@@ -121,9 +134,43 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
icon.Icon = FontAwesome.Solid.ArrowAltCircleUp;
icon.Colour = colours.BlueLighter;
break;
- }
- this.FadeIn(fade_time);
+ default:
+ throw new ArgumentOutOfRangeException(nameof(state), state, null);
+ }
+ }
+
+ private void showBeatmapAvailability(BeatmapAvailability availability)
+ {
+ switch (availability.State)
+ {
+ default:
+ this.FadeOut(fade_time);
+ break;
+
+ case DownloadState.NotDownloaded:
+ text.Text = "no map";
+ icon.Icon = FontAwesome.Solid.MinusCircle;
+ icon.Colour = colours.RedLight;
+ break;
+
+ case DownloadState.Downloading:
+ Debug.Assert(availability.DownloadProgress != null);
+
+ progressBar.FadeIn(fade_time);
+ progressBar.CurrentTime = availability.DownloadProgress.Value;
+
+ text.Text = "downloading map";
+ icon.Icon = FontAwesome.Solid.ArrowAltCircleDown;
+ icon.Colour = colours.Blue;
+ break;
+
+ case DownloadState.Importing:
+ text.Text = "importing map";
+ icon.Icon = FontAwesome.Solid.ArrowAltCircleDown;
+ icon.Colour = colours.Yellow;
+ break;
+ }
}
}
}
diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs
index f1120f55a6..4f53a6e202 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select.Carousel
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours)
{
- sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}");
+ sampleHover = audio.Samples.Get("SongSelect/song-ping");
hoverLayer.Colour = colours.Blue.Opacity(0.1f);
}
@@ -99,7 +99,11 @@ namespace osu.Game.Screens.Select.Carousel
protected override bool OnHover(HoverEvent e)
{
- sampleHover?.Play();
+ if (sampleHover != null)
+ {
+ sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2);
+ sampleHover.Play();
+ }
hoverLayer.FadeIn(100, Easing.OutQuint);
return base.OnHover(e);
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..f69613cfd3 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..1c602e1584 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -71,7 +71,7 @@
-
+