diff --git a/Directory.Build.props b/Directory.Build.props
index 235feea8ce..3b6b985961 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -17,7 +17,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index 52b728a115..a1c53ece03 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 95b96adab0..683e9fd5e8 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index d12403016d..b7a7fff18a 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 95b96adab0..683e9fd5e8 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs
index 2c4577f239..fe3e08537e 100644
--- a/osu.Desktop/DiscordRichPresence.cs
+++ b/osu.Desktop/DiscordRichPresence.cs
@@ -98,7 +98,7 @@ namespace osu.Desktop
if (status.Value is UserStatusOnline && activity.Value != null)
{
- presence.State = truncate(activity.Value.Status);
+ presence.State = truncate(activity.Value.GetStatus(privacyMode.Value == DiscordRichPresenceMode.Limited));
presence.Details = truncate(getDetails(activity.Value));
if (getBeatmap(activity.Value) is IBeatmapInfo beatmap && beatmap.OnlineID > 0)
@@ -169,7 +169,7 @@ namespace osu.Desktop
case UserActivity.InGame game:
return game.BeatmapInfo;
- case UserActivity.Editing edit:
+ case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo;
}
@@ -183,9 +183,12 @@ namespace osu.Desktop
case UserActivity.InGame game:
return game.BeatmapInfo.ToString() ?? string.Empty;
- case UserActivity.Editing edit:
+ case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo.ToString() ?? string.Empty;
+ case UserActivity.WatchingReplay watching:
+ return watching.BeatmapInfo.ToString();
+
case UserActivity.InLobby lobby:
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 1f4544098b..f1b9c92429 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -26,8 +26,8 @@
-
-
+
+
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index f47b069373..4719d54138 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -7,9 +7,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 5a2e8e0bf0..01922b2a96 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index be51dc0e4c..027bf60a0c 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index c10c3ffb15..57900bffd7 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -1,10 +1,10 @@
-
-
+
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
index 796f5721bb..781a686700 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
@@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestCase("slider-conversion-v6")]
[TestCase("slider-conversion-v14")]
[TestCase("slider-generating-drumroll-2")]
+ [TestCase("file-hitsamples")]
public void Test(string name) => base.Test(name);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 6af1beff69..0c39ad988b 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index 2ccdfd40e5..d0361b1c8d 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -2,13 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods
{
- public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset
+ public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IApplicableToDrawableHitObject
{
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
@@ -18,5 +20,11 @@ namespace osu.Game.Rulesets.Taiko.Mods
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;
}
+
+ public void ApplyToDrawableHitObject(DrawableHitObject drawable)
+ {
+ if (drawable is DrawableTaikoHitObject hit)
+ hit.SnapJudgementLocation = true;
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
index ff4edf35fa..62c8457c58 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
@@ -207,6 +207,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
const float gravity_time = 300;
const float gravity_travel_height = 200;
+ if (SnapJudgementLocation)
+ MainPiece.MoveToX(-X);
+
this.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad);
this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 6172b75d2c..f695c505a4 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -25,6 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private readonly Container nonProxiedContent;
+ ///
+ /// Whether the location of the hit should be snapped to the hit target before animating.
+ ///
+ ///
+ /// This is how osu-stable worked, but notably is not how TnT works.
+ /// Not snapping results in less visual feedback on hit accuracy.
+ ///
+ public bool SnapJudgementLocation { get; set; }
+
protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject)
: base(hitObject)
{
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples-expected-conversion.json
new file mode 100644
index 0000000000..70348a3871
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples-expected-conversion.json
@@ -0,0 +1 @@
+{"Mappings":[{"StartTime":500.0,"Objects":[{"StartTime":500.0,"EndTime":500.0,"IsRim":false,"IsCentre":true,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":1000.0,"Objects":[{"StartTime":1000.0,"EndTime":1000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":1500.0,"Objects":[{"StartTime":1500.0,"EndTime":1500.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":2000.0,"Objects":[{"StartTime":2000.0,"EndTime":2000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":2500.0,"Objects":[{"StartTime":2500.0,"EndTime":2500.0,"IsRim":false,"IsCentre":true,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":3000.0,"Objects":[{"StartTime":3000.0,"EndTime":3000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":3500.0,"Objects":[{"StartTime":3500.0,"EndTime":3500.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":4000.0,"Objects":[{"StartTime":4000.0,"EndTime":4000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]}]}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples.osu
new file mode 100644
index 0000000000..5d4bcb52a1
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/file-hitsamples.osu
@@ -0,0 +1,22 @@
+osu file format v14
+
+[Difficulty]
+HPDrainRate:5
+CircleSize:7
+OverallDifficulty:6.5
+ApproachRate:10
+SliderMultiplier:1.9
+SliderTickRate:1
+
+[TimingPoints]
+500,500,4,2,1,50,1,0
+
+[HitObjects]
+256,192,500,1,0,0:0:0:0:sample.ogg
+256,192,1000,1,8,0:0:0:0:sample.ogg
+256,192,1500,1,2,0:0:0:0:sample.ogg
+256,192,2000,1,10,0:0:0:0:sample.ogg
+256,192,2500,1,4,0:0:0:0:sample.ogg
+256,192,3000,1,12,0:0:0:0:sample.ogg
+256,192,3500,1,6,0:0:0:0:sample.ogg
+256,192,4000,1,14,0:0:0:0:sample.ogg
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 9944c0c6b7..4f435e73b3 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -209,9 +209,8 @@ namespace osu.Game.Rulesets.Taiko
HitResult.Great,
HitResult.Ok,
- HitResult.SmallTickHit,
-
HitResult.SmallBonus,
+ HitResult.LargeBonus,
};
}
@@ -220,6 +219,9 @@ namespace osu.Game.Rulesets.Taiko
switch (result)
{
case HitResult.SmallBonus:
+ return "drum tick";
+
+ case HitResult.LargeBonus:
return "bonus";
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs
new file mode 100644
index 0000000000..ce93837925
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs
@@ -0,0 +1,24 @@
+// 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.Shapes;
+using osu.Game.Screens.Play.Break;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public partial class TestSceneLetterboxOverlay : OsuTestScene
+ {
+ public TestSceneLetterboxOverlay()
+ {
+ AddRange(new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ new LetterboxOverlay()
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
index 334d01f915..3e415af86e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
@@ -1,32 +1,64 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Beatmaps;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneReplayPlayer : RateAdjustedBeatmapTestScene
{
- protected TestReplayPlayer Player;
-
- public override void SetUpSteps()
- {
- base.SetUpSteps();
-
- AddStep("Initialise player", () => Player = CreatePlayer(new OsuRuleset()));
- AddStep("Load player", () => LoadScreen(Player));
- AddUntilStep("player loaded", () => Player.IsLoaded);
- }
+ protected TestReplayPlayer Player = null!;
[Test]
public void TestPauseViaSpace()
{
+ loadPlayerWithBeatmap();
+
+ double? lastTime = null;
+
+ AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
+
+ AddStep("Pause playback with space", () => InputManager.Key(Key.Space));
+
+ AddAssert("player not exited", () => Player.IsCurrentScreen());
+
+ AddUntilStep("Time stopped progressing", () =>
+ {
+ double current = Player.GameplayClockContainer.CurrentTime;
+ bool changed = lastTime != current;
+ lastTime = current;
+
+ return !changed;
+ });
+
+ AddWaitStep("wait some", 10);
+
+ AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime);
+ }
+
+ [Test]
+ public void TestPauseViaSpaceWithSkip()
+ {
+ loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
+ {
+ BeatmapInfo = { AudioLeadIn = 60000 }
+ });
+
+ AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType().First().IsButtonVisible);
+
+ AddStep("Skip with space", () => InputManager.Key(Key.Space));
+
+ AddAssert("Player not paused", () => !Player.DrawableRuleset.IsPaused.Value);
+
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@@ -52,6 +84,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestPauseViaMiddleMouse()
{
+ loadPlayerWithBeatmap();
+
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@@ -77,6 +111,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestSeekBackwards()
{
+ loadPlayerWithBeatmap();
+
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@@ -93,6 +129,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestSeekForwards()
{
+ loadPlayerWithBeatmap();
+
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@@ -106,12 +144,26 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Jumped forwards", () => Player.GameplayClockContainer.CurrentTime - lastTime > 500);
}
- protected TestReplayPlayer CreatePlayer(Ruleset ruleset)
+ private void loadPlayerWithBeatmap(IBeatmap? beatmap = null)
{
- Beatmap.Value = CreateWorkingBeatmap(ruleset.RulesetInfo);
+ AddStep("create player", () =>
+ {
+ CreatePlayer(new OsuRuleset(), beatmap);
+ });
+
+ AddStep("Load player", () => LoadScreen(Player));
+ AddUntilStep("player loaded", () => Player.IsLoaded);
+ }
+
+ protected void CreatePlayer(Ruleset ruleset, IBeatmap? beatmap = null)
+ {
+ Beatmap.Value = beatmap != null
+ ? CreateWorkingBeatmap(beatmap)
+ : CreateWorkingBeatmap(ruleset.RulesetInfo);
+
SelectedMods.Value = new[] { ruleset.GetAutoplayMod() };
- return new TestReplayPlayer(false);
+ Player = new TestReplayPlayer(false);
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
index 4675410164..10c2b2b9e1 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestEditActivity()
{
- AddStep("Set activity", () => api.Activity.Value = new UserActivity.Editing(new BeatmapInfo()));
+ AddStep("Set activity", () => api.Activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
index 4c1df850b2..a047e2f0c5 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
@@ -11,6 +11,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
+using osu.Game.Scoring;
+using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
using osuTK;
@@ -107,14 +109,16 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set online status", () => status.Value = new UserStatusOnline());
AddStep("idle", () => activity.Value = null);
- AddStep("spectating", () => activity.Value = new UserActivity.Spectating());
+ AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats")));
+ AddStep("spectating user", () => activity.Value = new UserActivity.SpectatingUser(createScore(@"mrekk")));
AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0));
AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1));
AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2));
AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3));
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
- AddStep("editing", () => activity.Value = new UserActivity.Editing(null));
- AddStep("modding", () => activity.Value = new UserActivity.Modding());
+ AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(null));
+ AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(null));
+ AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(null, null));
}
[Test]
@@ -132,6 +136,14 @@ namespace osu.Game.Tests.Visual.Online
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(null, rulesetStore.GetRuleset(rulesetId));
+ private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo)
+ {
+ User = new APIUser
+ {
+ Username = name,
+ }
+ };
+
private partial class TestUserListPanel : UserListPanel
{
public TestUserListPanel(APIUser user)
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
index 0145a1dfef..bf18bd3e51 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
@@ -24,17 +24,26 @@ namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneAccuracyCircle : OsuTestScene
{
- [TestCase(0.2, ScoreRank.D)]
- [TestCase(0.5, ScoreRank.D)]
- [TestCase(0.75, ScoreRank.C)]
- [TestCase(0.85, ScoreRank.B)]
- [TestCase(0.925, ScoreRank.A)]
- [TestCase(0.975, ScoreRank.S)]
- [TestCase(0.9999, ScoreRank.S)]
- [TestCase(1, ScoreRank.X)]
- public void TestRank(double accuracy, ScoreRank rank)
+ [TestCase(0)]
+ [TestCase(0.2)]
+ [TestCase(0.5)]
+ [TestCase(0.6999)]
+ [TestCase(0.7)]
+ [TestCase(0.75)]
+ [TestCase(0.7999)]
+ [TestCase(0.8)]
+ [TestCase(0.85)]
+ [TestCase(0.8999)]
+ [TestCase(0.9)]
+ [TestCase(0.925)]
+ [TestCase(0.9499)]
+ [TestCase(0.95)]
+ [TestCase(0.975)]
+ [TestCase(0.9999)]
+ [TestCase(1)]
+ public void TestRank(double accuracy)
{
- var score = createScore(accuracy, rank);
+ var score = createScore(accuracy, ScoreProcessor.RankFromAccuracy(accuracy));
addCircleStep(score);
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs
new file mode 100644
index 0000000000..72adbfc104
--- /dev/null
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooterV2.cs
@@ -0,0 +1,146 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Mods;
+using osu.Game.Screens.Select.FooterV2;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.SongSelect
+{
+ public partial class TestSceneSongSelectFooterV2 : OsuManualInputManagerTestScene
+ {
+ private FooterButtonRandomV2 randomButton = null!;
+ private FooterButtonModsV2 modsButton = null!;
+
+ private bool nextRandomCalled;
+ private bool previousRandomCalled;
+
+ private DummyOverlay overlay = null!;
+
+ [Cached]
+ private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ nextRandomCalled = false;
+ previousRandomCalled = false;
+
+ FooterV2 footer;
+
+ Children = new Drawable[]
+ {
+ footer = new FooterV2
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ },
+ overlay = new DummyOverlay()
+ };
+
+ footer.AddButton(modsButton = new FooterButtonModsV2(), overlay);
+ footer.AddButton(randomButton = new FooterButtonRandomV2
+ {
+ NextRandom = () => nextRandomCalled = true,
+ PreviousRandom = () => previousRandomCalled = true
+ });
+ footer.AddButton(new FooterButtonOptionsV2());
+
+ overlay.Hide();
+ });
+
+ [Test]
+ public void TestState()
+ {
+ AddToggleStep("set options enabled state", state => this.ChildrenOfType().Last().Enabled.Value = state);
+ }
+
+ [Test]
+ public void TestFooterRandom()
+ {
+ AddStep("press F2", () => InputManager.Key(Key.F2));
+ AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRandomViaMouse()
+ {
+ AddStep("click button", () =>
+ {
+ InputManager.MoveMouseTo(randomButton);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRewind()
+ {
+ AddStep("press Shift+F2", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.PressKey(Key.F2);
+ InputManager.ReleaseKey(Key.F2);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRewindViaShiftMouseLeft()
+ {
+ AddStep("shift + click button", () =>
+ {
+ InputManager.PressKey(Key.LShift);
+ InputManager.MoveMouseTo(randomButton);
+ InputManager.Click(MouseButton.Left);
+ InputManager.ReleaseKey(Key.LShift);
+ });
+ AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
+ }
+
+ [Test]
+ public void TestFooterRewindViaMouseRight()
+ {
+ AddStep("right click button", () =>
+ {
+ InputManager.MoveMouseTo(randomButton);
+ InputManager.Click(MouseButton.Right);
+ });
+ AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
+ }
+
+ [Test]
+ public void TestOverlayPresent()
+ {
+ AddStep("Press F1", () =>
+ {
+ InputManager.MoveMouseTo(modsButton);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddAssert("Overlay visible", () => overlay.State.Value == Visibility.Visible);
+ AddStep("Hide", () => overlay.Hide());
+ }
+
+ private partial class DummyOverlay : ShearedOverlayContainer
+ {
+ public DummyOverlay()
+ : base(OverlayColourScheme.Green)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Header.Title = "An overlay";
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 24969414d0..59a786a11d 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -2,11 +2,11 @@
-
+
-
-
+
+
WinExe
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 9f2a088a4b..5847079161 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -4,9 +4,9 @@
osu.Game.Tournament.Tests.TournamentTestRunner
-
+
-
+
WinExe
diff --git a/osu.Game/Audio/PreviewTrack.cs b/osu.Game/Audio/PreviewTrack.cs
index 2c63c16274..d625566ee7 100644
--- a/osu.Game/Audio/PreviewTrack.cs
+++ b/osu.Game/Audio/PreviewTrack.cs
@@ -98,6 +98,9 @@ namespace osu.Game.Audio
Track.Stop();
+ // Ensure the track is reset immediately on stopping, so the next time it is started it has a correct time value.
+ Track.Seek(0);
+
Stopped?.Invoke();
}
diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs
index cbe327bac7..9f21512825 100644
--- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs
+++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs
@@ -100,9 +100,9 @@ namespace osu.Game.Graphics.Containers
///
/// Abort any ongoing confirmation. Should be called when the container's interaction is no longer valid (ie. the user releases a key).
///
- protected void AbortConfirm()
+ protected virtual void AbortConfirm()
{
- if (!AllowMultipleFires && Fired) return;
+ if (!confirming || (!AllowMultipleFires && Fired)) return;
confirming = false;
Fired = false;
diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
index 6fe1de2216..fceee90d06 100644
--- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
@@ -46,8 +46,8 @@ namespace osu.Game.Graphics.Containers
AddRangeInternal(new Drawable[]
{
+ CreateHoverSounds(sampleSet),
content,
- CreateHoverSounds(sampleSet)
});
}
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 9d14ce95cf..d580eea248 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -35,8 +35,8 @@ namespace osu.Game.Input.Bindings
// It is used to decide the order of precedence, with the earlier items having higher precedence.
public override IEnumerable DefaultKeyBindings => GlobalKeyBindings
.Concat(EditorKeyBindings)
- .Concat(ReplayKeyBindings)
.Concat(InGameKeyBindings)
+ .Concat(ReplayKeyBindings)
.Concat(SongSelectKeyBindings)
.Concat(AudioControlKeyBindings)
// Overlay bindings may conflict with more local cases like the editor so they are checked last.
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 7ab678775f..e95bc128c8 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat
{
connector.ChannelJoined += ch => Schedule(() => joinChannel(ch));
- connector.ChannelParted += ch => Schedule(() => LeaveChannel(getChannel(ch)));
+ connector.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
@@ -558,7 +558,9 @@ namespace osu.Game.Online.Chat
/// Leave the specified channel. Can be called from any thread.
///
/// The channel to leave.
- public void LeaveChannel(Channel channel) => Schedule(() =>
+ public void LeaveChannel(Channel channel) => Schedule(() => leaveChannel(channel, true));
+
+ private void leaveChannel(Channel channel, bool sendLeaveRequest)
{
if (channel == null) return;
@@ -581,10 +583,11 @@ namespace osu.Game.Online.Chat
if (channel.Joined.Value)
{
- api.Queue(new LeaveChannelRequest(channel));
+ if (sendLeaveRequest)
+ api.Queue(new LeaveChannelRequest(channel));
channel.Joined.Value = false;
}
- });
+ }
///
/// Opens the most recently closed channel that has not already been reopened,
diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs
index 9902704883..e7018d6993 100644
--- a/osu.Game/Online/Chat/NowPlayingCommand.cs
+++ b/osu.Game/Online/Chat/NowPlayingCommand.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Online.Chat
beatmapInfo = game.BeatmapInfo;
break;
- case UserActivity.Editing edit:
+ case UserActivity.EditingBeatmap edit:
verb = "editing";
beatmapInfo = edit.BeatmapInfo;
break;
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index df3d8b99f4..7c9b03bd5b 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -60,6 +60,7 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
+using osu.Game.Skinning;
using osu.Game.Updater;
using osu.Game.Users;
using osu.Game.Utils;
@@ -501,6 +502,23 @@ namespace osu.Game
/// The build version of the update stream
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
+ ///
+ /// Present a skin select immediately.
+ ///
+ /// The skin to select.
+ public void PresentSkin(SkinInfo skin)
+ {
+ var databasedSkin = SkinManager.Query(s => s.ID == skin.ID);
+
+ if (databasedSkin == null)
+ {
+ Logger.Log("The requested skin could not be loaded.", LoggingTarget.Information);
+ return;
+ }
+
+ SkinManager.CurrentSkinInfo.Value = databasedSkin;
+ }
+
///
/// Present a beatmap at song select immediately.
/// The user should have already requested this interactively.
@@ -777,6 +795,7 @@ namespace osu.Game
// todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => Notifications.Post(n);
+ SkinManager.PresentImport = items => PresentSkin(items.First().Value);
BeatmapManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value);
diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs
index 6b3716ac8d..19d7ea7a87 100644
--- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs
+++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs
@@ -57,6 +57,7 @@ namespace osu.Game.Overlays.Dialog
private Sample confirmSample;
private double lastTickPlaybackTime;
private AudioFilter lowPassFilter = null!;
+ private bool mouseDown;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
@@ -73,6 +74,12 @@ namespace osu.Game.Overlays.Dialog
Progress.BindValueChanged(progressChanged);
}
+ protected override void AbortConfirm()
+ {
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
+ base.AbortConfirm();
+ }
+
protected override void Confirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
@@ -83,6 +90,7 @@ namespace osu.Game.Overlays.Dialog
protected override bool OnMouseDown(MouseDownEvent e)
{
BeginConfirm();
+ mouseDown = true;
return true;
}
@@ -90,11 +98,28 @@ namespace osu.Game.Overlays.Dialog
{
if (!e.HasAnyButtonPressed)
{
- lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
AbortConfirm();
+ mouseDown = false;
}
}
+ protected override bool OnHover(HoverEvent e)
+ {
+ if (mouseDown)
+ BeginConfirm();
+
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ base.OnHoverLost(e);
+
+ if (!mouseDown) return;
+
+ AbortConfirm();
+ }
+
private void progressChanged(ValueChangedEvent progress)
{
if (progress.NewValue < progress.OldValue) return;
diff --git a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs
index a8d64c1de8..28ceaf09fc 100644
--- a/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs
+++ b/osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs
@@ -148,9 +148,9 @@ namespace osu.Game.Overlays.SkinEditor
component.Origin = Anchor.Centre;
}
- protected override void Update()
+ protected override void UpdateAfterChildren()
{
- base.Update();
+ base.UpdateAfterChildren();
if (component.DrawSize != Vector2.Zero)
{
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 930ee0448f..68ca6bc506 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -439,19 +439,20 @@ namespace osu.Game.Rulesets.Objects.Legacy
private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo)
{
- // Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario
- if (!string.IsNullOrEmpty(bankInfo.Filename))
- {
- return new List { new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume) };
- }
+ var soundTypes = new List();
- var soundTypes = new List
+ if (string.IsNullOrEmpty(bankInfo.Filename))
{
- new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
- // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
- // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
- type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal))
- };
+ soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
+ // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
+ // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
+ type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal)));
+ }
+ else
+ {
+ // Todo: This should set the normal SampleInfo if the specified sample file isn't found, but that's a pretty edge-case scenario
+ soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume));
+ }
if (type.HasFlagFast(LegacyHitSoundType.Finish))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index b5f42ec2cc..96f6922224 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -7,21 +7,28 @@ using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
+using osu.Game.Localisation;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Scoring;
-using osu.Framework.Localisation;
-using osu.Game.Localisation;
namespace osu.Game.Rulesets.Scoring
{
public partial class ScoreProcessor : JudgementProcessor
{
+ private const double accuracy_cutoff_x = 1;
+ private const double accuracy_cutoff_s = 0.95;
+ private const double accuracy_cutoff_a = 0.9;
+ private const double accuracy_cutoff_b = 0.8;
+ private const double accuracy_cutoff_c = 0.7;
+ private const double accuracy_cutoff_d = 0;
+
private const double max_score = 1000000;
///
@@ -160,7 +167,7 @@ namespace osu.Game.Rulesets.Scoring
Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue);
Accuracy.ValueChanged += accuracy =>
{
- Rank.Value = rankFrom(accuracy.NewValue);
+ Rank.Value = RankFromAccuracy(accuracy.NewValue);
foreach (var mod in Mods.Value.OfType())
Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue);
};
@@ -369,22 +376,6 @@ namespace osu.Game.Rulesets.Scoring
}
}
- private ScoreRank rankFrom(double acc)
- {
- if (acc == 1)
- return ScoreRank.X;
- if (acc >= 0.95)
- return ScoreRank.S;
- if (acc >= 0.9)
- return ScoreRank.A;
- if (acc >= 0.8)
- return ScoreRank.B;
- if (acc >= 0.7)
- return ScoreRank.C;
-
- return ScoreRank.D;
- }
-
///
/// Resets this ScoreProcessor to a default state.
///
@@ -583,6 +574,62 @@ namespace osu.Game.Rulesets.Scoring
hitEvents.Clear();
}
+ #region Static helper methods
+
+ ///
+ /// Given an accuracy (0..1), return the correct .
+ ///
+ public static ScoreRank RankFromAccuracy(double accuracy)
+ {
+ if (accuracy == accuracy_cutoff_x)
+ return ScoreRank.X;
+ if (accuracy >= accuracy_cutoff_s)
+ return ScoreRank.S;
+ if (accuracy >= accuracy_cutoff_a)
+ return ScoreRank.A;
+ if (accuracy >= accuracy_cutoff_b)
+ return ScoreRank.B;
+ if (accuracy >= accuracy_cutoff_c)
+ return ScoreRank.C;
+
+ return ScoreRank.D;
+ }
+
+ ///
+ /// Given a , return the cutoff accuracy (0..1).
+ /// Accuracy must be greater than or equal to the cutoff to qualify for the provided rank.
+ ///
+ public static double AccuracyCutoffFromRank(ScoreRank rank)
+ {
+ switch (rank)
+ {
+ case ScoreRank.X:
+ case ScoreRank.XH:
+ return accuracy_cutoff_x;
+
+ case ScoreRank.S:
+ case ScoreRank.SH:
+ return accuracy_cutoff_s;
+
+ case ScoreRank.A:
+ return accuracy_cutoff_a;
+
+ case ScoreRank.B:
+ return accuracy_cutoff_b;
+
+ case ScoreRank.C:
+ return accuracy_cutoff_c;
+
+ case ScoreRank.D:
+ return accuracy_cutoff_d;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(rank), rank, null);
+ }
+ }
+
+ #endregion
+
///
/// Stores the required scoring data that fulfils the minimum requirements for a to calculate score.
///
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 0622cbebae..bd133383d1 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -157,7 +157,16 @@ namespace osu.Game.Screens.Edit
private bool isNewBeatmap;
- protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo);
+ protected override UserActivity InitialActivity
+ {
+ get
+ {
+ if (Beatmap.Value.Metadata.Author.OnlineID == api.LocalUser.Value.OnlineID)
+ return new UserActivity.EditingBeatmap(Beatmap.Value.BeatmapInfo);
+
+ return new UserActivity.ModdingBeatmap(Beatmap.Value.BeatmapInfo);
+ }
+ }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
index e7db1c105b..7dff05667d 100644
--- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
+++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
@@ -7,6 +7,7 @@ using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Screens.Play;
+using osu.Game.Users;
namespace osu.Game.Screens.Edit.GameplayTest
{
@@ -15,6 +16,8 @@ namespace osu.Game.Screens.Edit.GameplayTest
private readonly Editor editor;
private readonly EditorState editorState;
+ protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo, Ruleset.Value);
+
[Resolved]
private MusicController musicController { get; set; } = null!;
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs
new file mode 100644
index 0000000000..555c36aac0
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs
@@ -0,0 +1,209 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ public partial class ControlPointList : CompositeDrawable
+ {
+ private OsuButton deleteButton = null!;
+ private ControlPointTable table = null!;
+ private OsuScrollContainer scroll = null!;
+ private RoundedButton addButton = null!;
+
+ private readonly IBindableList controlPointGroups = new BindableList();
+
+ [Resolved]
+ private EditorClock clock { get; set; } = null!;
+
+ [Resolved]
+ protected EditorBeatmap Beatmap { get; private set; } = null!;
+
+ [Resolved]
+ private Bindable selectedGroup { get; set; } = null!;
+
+ [Resolved]
+ private IEditorChangeHandler? changeHandler { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ const float margins = 10;
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Background4,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Box
+ {
+ Colour = colours.Background3,
+ RelativeSizeAxes = Axes.Y,
+ Width = ControlPointTable.TIMING_COLUMN_WIDTH + margins,
+ },
+ scroll = new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = table = new ControlPointTable(),
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Direction = FillDirection.Horizontal,
+ Margin = new MarginPadding(margins),
+ Spacing = new Vector2(5),
+ Children = new Drawable[]
+ {
+ deleteButton = new RoundedButton
+ {
+ Text = "-",
+ Size = new Vector2(30, 30),
+ Action = delete,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ addButton = new RoundedButton
+ {
+ Action = addNew,
+ Size = new Vector2(160, 30),
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ }
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedGroup.BindValueChanged(selected =>
+ {
+ deleteButton.Enabled.Value = selected.NewValue != null;
+
+ addButton.Text = selected.NewValue != null
+ ? "+ Clone to current time"
+ : "+ Add at current time";
+ }, true);
+
+ controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups);
+ controlPointGroups.BindCollectionChanged((_, _) =>
+ {
+ table.ControlGroups = controlPointGroups;
+ changeHandler?.SaveState();
+ }, true);
+
+ table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ selectedGroup.Value = null;
+ return true;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ trackActivePoint();
+
+ addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
+ }
+
+ private Type? trackedType;
+
+ ///
+ /// Given the user has selected a control point group, we want to track any group which is
+ /// active at the current point in time which matches the type the user has selected.
+ ///
+ /// So if the user is currently looking at a timing point and seeks into the future, a
+ /// future timing point would be automatically selected if it is now the new "current" point.
+ ///
+ private void trackActivePoint()
+ {
+ // For simplicity only match on the first type of the active control point.
+ if (selectedGroup.Value == null)
+ trackedType = null;
+ else
+ {
+ // If the selected group only has one control point, update the tracking type.
+ if (selectedGroup.Value.ControlPoints.Count == 1)
+ trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
+ // If the selected group has more than one control point, choose the first as the tracking type
+ // if we don't already have a singular tracked type.
+ else if (trackedType == null)
+ trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
+ }
+
+ if (trackedType != null)
+ {
+ // We don't have an efficient way of looking up groups currently, only individual point types.
+ // To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
+
+ // Find the next group which has the same type as the selected one.
+ var found = Beatmap.ControlPointInfo.Groups
+ .Where(g => g.ControlPoints.Any(cp => cp.GetType() == trackedType))
+ .LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
+
+ if (found != null)
+ selectedGroup.Value = found;
+ }
+ }
+
+ private void delete()
+ {
+ if (selectedGroup.Value == null)
+ return;
+
+ Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
+
+ selectedGroup.Value = Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime);
+ }
+
+ private void addNew()
+ {
+ bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any();
+
+ var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
+
+ if (isFirstControlPoint)
+ group.Add(new TimingControlPoint());
+ else
+ {
+ // Try and create matching types from the currently selected control point.
+ var selected = selectedGroup.Value;
+
+ if (selected != null && !ReferenceEquals(selected, group))
+ {
+ foreach (var controlPoint in selected.ControlPoints)
+ {
+ group.Add(controlPoint.DeepClone());
+ }
+ }
+ }
+
+ selectedGroup.Value = group;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
index 2450909929..3f911f5067 100644
--- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs
+++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs
@@ -1,20 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
-using System.Linq;
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.ControlPoints;
-using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Graphics.UserInterfaceV2;
-using osu.Game.Overlays;
-using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
@@ -23,6 +14,9 @@ namespace osu.Game.Screens.Edit.Timing
[Cached]
public readonly Bindable SelectedGroup = new Bindable();
+ [Resolved]
+ private EditorClock? editorClock { get; set; }
+
public TimingScreen()
: base(EditorScreenMode.Timing)
{
@@ -46,192 +40,17 @@ namespace osu.Game.Screens.Edit.Timing
}
};
- public partial class ControlPointList : CompositeDrawable
+ protected override void LoadComplete()
{
- private OsuButton deleteButton = null!;
- private ControlPointTable table = null!;
- private OsuScrollContainer scroll = null!;
- private RoundedButton addButton = null!;
+ base.LoadComplete();
- private readonly IBindableList controlPointGroups = new BindableList();
-
- [Resolved]
- private EditorClock clock { get; set; } = null!;
-
- [Resolved]
- protected EditorBeatmap Beatmap { get; private set; } = null!;
-
- [Resolved]
- private Bindable selectedGroup { get; set; } = null!;
-
- [Resolved]
- private IEditorChangeHandler? changeHandler { get; set; }
-
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colours)
+ if (editorClock != null)
{
- RelativeSizeAxes = Axes.Both;
-
- const float margins = 10;
- InternalChildren = new Drawable[]
- {
- new Box
- {
- Colour = colours.Background4,
- RelativeSizeAxes = Axes.Both,
- },
- new Box
- {
- Colour = colours.Background3,
- RelativeSizeAxes = Axes.Y,
- Width = ControlPointTable.TIMING_COLUMN_WIDTH + margins,
- },
- scroll = new OsuScrollContainer
- {
- RelativeSizeAxes = Axes.Both,
- Child = table = new ControlPointTable(),
- },
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- Direction = FillDirection.Horizontal,
- Margin = new MarginPadding(margins),
- Spacing = new Vector2(5),
- Children = new Drawable[]
- {
- deleteButton = new RoundedButton
- {
- Text = "-",
- Size = new Vector2(30, 30),
- Action = delete,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- },
- addButton = new RoundedButton
- {
- Action = addNew,
- Size = new Vector2(160, 30),
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- },
- }
- },
- };
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- selectedGroup.BindValueChanged(selected =>
- {
- deleteButton.Enabled.Value = selected.NewValue != null;
-
- addButton.Text = selected.NewValue != null
- ? "+ Clone to current time"
- : "+ Add at current time";
- }, true);
-
- controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups);
- controlPointGroups.BindCollectionChanged((_, _) =>
- {
- table.ControlGroups = controlPointGroups;
- changeHandler?.SaveState();
- }, true);
-
- table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable);
- }
-
- protected override bool OnClick(ClickEvent e)
- {
- selectedGroup.Value = null;
- return true;
- }
-
- protected override void Update()
- {
- base.Update();
-
- trackActivePoint();
-
- addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
- }
-
- private Type? trackedType;
-
- ///
- /// Given the user has selected a control point group, we want to track any group which is
- /// active at the current point in time which matches the type the user has selected.
- ///
- /// So if the user is currently looking at a timing point and seeks into the future, a
- /// future timing point would be automatically selected if it is now the new "current" point.
- ///
- private void trackActivePoint()
- {
- // For simplicity only match on the first type of the active control point.
- if (selectedGroup.Value == null)
- trackedType = null;
- else
- {
- // If the selected group only has one control point, update the tracking type.
- if (selectedGroup.Value.ControlPoints.Count == 1)
- trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
- // If the selected group has more than one control point, choose the first as the tracking type
- // if we don't already have a singular tracked type.
- else if (trackedType == null)
- trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
- }
-
- if (trackedType != null)
- {
- // We don't have an efficient way of looking up groups currently, only individual point types.
- // To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
-
- // Find the next group which has the same type as the selected one.
- var found = Beatmap.ControlPointInfo.Groups
- .Where(g => g.ControlPoints.Any(cp => cp.GetType() == trackedType))
- .LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
-
- if (found != null)
- selectedGroup.Value = found;
- }
- }
-
- private void delete()
- {
- if (selectedGroup.Value == null)
- return;
-
- Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
-
- selectedGroup.Value = Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime);
- }
-
- private void addNew()
- {
- bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any();
-
- var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
-
- if (isFirstControlPoint)
- group.Add(new TimingControlPoint());
- else
- {
- // Try and create matching types from the currently selected control point.
- var selected = selectedGroup.Value;
-
- if (selected != null && !ReferenceEquals(selected, group))
- {
- foreach (var controlPoint in selected.ControlPoints)
- {
- group.Add(controlPoint.DeepClone());
- }
- }
- }
-
- selectedGroup.Value = group;
+ // When entering the timing screen, let's choose the closest valid timing point.
+ // This will emulate the osu-stable behaviour where a metronome and timing information
+ // are presented on entering the screen.
+ var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime);
+ SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time);
}
}
}
diff --git a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs
index 92b432831d..c4e2dbf403 100644
--- a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs
+++ b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -22,29 +20,21 @@ namespace osu.Game.Screens.Play.Break
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
- new Container
+ new Box
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
RelativeSizeAxes = Axes.X,
Height = height,
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black),
- }
+ Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black),
},
- new Container
+ new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = height,
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black),
- }
+ Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black),
}
};
}
diff --git a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
index 7f9f353ded..c04ecd671f 100644
--- a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
+++ b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
@@ -121,8 +121,8 @@ namespace osu.Game.Screens.Play.HUD
AutoSizeAxes = Axes.Both,
Child = new UprightAspectMaintainingContainer
{
- Origin = Anchor.CentreRight,
- Anchor = Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Scaling = ScaleMode.Vertical,
ScalingFactor = 0.5f,
diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs
index c5ef6b1585..8a4e63d21c 100644
--- a/osu.Game/Screens/Play/ReplayPlayer.cs
+++ b/osu.Game/Screens/Play/ReplayPlayer.cs
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
+using osu.Game.Users;
namespace osu.Game.Screens.Play
{
@@ -24,6 +25,8 @@ namespace osu.Game.Screens.Play
private readonly bool replayIsFailedScore;
+ protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo);
+
// Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108)
protected override bool CheckModsAllowFailure()
{
diff --git a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs
index 240fbcf662..c9d1f4acaa 100644
--- a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs
+++ b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.Spectator;
using osu.Game.Scoring;
+using osu.Game.Users;
namespace osu.Game.Screens.Play
{
@@ -14,6 +15,8 @@ namespace osu.Game.Screens.Play
{
private readonly Score score;
+ protected override UserActivity InitialActivity => new UserActivity.SpectatingUser(Score.ScoreInfo);
+
public SoloSpectatorPlayer(Score score, PlayerConfiguration configuration = null)
: base(score, configuration)
{
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
index 8e04bb68fb..2ec4270c3c 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
@@ -17,6 +17,7 @@ using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osuTK;
@@ -28,6 +29,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
///
public partial class AccuracyCircle : CompositeDrawable
{
+ private static readonly double accuracy_x = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.X);
+ private static readonly double accuracy_s = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.S);
+ private static readonly double accuracy_a = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.A);
+ private static readonly double accuracy_b = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.B);
+ private static readonly double accuracy_c = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.C);
+ private static readonly double accuracy_d = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.D);
+
///
/// Duration for the transforms causing this component to appear.
///
@@ -73,6 +81,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
///
private const double virtual_ss_percentage = 0.01;
+ ///
+ /// The width of a in terms of accuracy.
+ ///
+ public const double NOTCH_WIDTH_PERCENTAGE = 1.0 / 360;
+
///
/// The easing for the circle filling transforms.
///
@@ -145,49 +158,49 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.X),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 1 }
+ Current = { Value = accuracy_x }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.S),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 1 - virtual_ss_percentage }
+ Current = { Value = accuracy_x - virtual_ss_percentage }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.A),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 0.95f }
+ Current = { Value = accuracy_s }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.B),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 0.9f }
+ Current = { Value = accuracy_a }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.C),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 0.8f }
+ Current = { Value = accuracy_b }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.D),
InnerRadius = RANK_CIRCLE_RADIUS,
- Current = { Value = 0.7f }
+ Current = { Value = accuracy_c }
},
- new RankNotch(0),
- new RankNotch((float)(1 - virtual_ss_percentage)),
- new RankNotch(0.95f),
- new RankNotch(0.9f),
- new RankNotch(0.8f),
- new RankNotch(0.7f),
+ new RankNotch((float)accuracy_x),
+ new RankNotch((float)(accuracy_x - virtual_ss_percentage)),
+ new RankNotch((float)accuracy_s),
+ new RankNotch((float)accuracy_a),
+ new RankNotch((float)accuracy_b),
+ new RankNotch((float)accuracy_c),
new BufferedContainer
{
Name = "Graded circle mask",
@@ -215,12 +228,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Padding = new MarginPadding { Vertical = -15, Horizontal = -20 },
Children = new[]
{
- new RankBadge(1, getRank(ScoreRank.X)),
- new RankBadge(0.95, getRank(ScoreRank.S)),
- new RankBadge(0.9, getRank(ScoreRank.A)),
- new RankBadge(0.8, getRank(ScoreRank.B)),
- new RankBadge(0.7, getRank(ScoreRank.C)),
- new RankBadge(0.35, getRank(ScoreRank.D)),
+ // The S and A badges are moved down slightly to prevent collision with the SS badge.
+ new RankBadge(accuracy_x, accuracy_x, getRank(ScoreRank.X)),
+ new RankBadge(accuracy_s, Interpolation.Lerp(accuracy_s, (accuracy_x - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)),
+ new RankBadge(accuracy_a, Interpolation.Lerp(accuracy_a, accuracy_s, 0.25), getRank(ScoreRank.A)),
+ new RankBadge(accuracy_b, Interpolation.Lerp(accuracy_b, accuracy_a, 0.5), getRank(ScoreRank.B)),
+ new RankBadge(accuracy_c, Interpolation.Lerp(accuracy_c, accuracy_b, 0.5), getRank(ScoreRank.C)),
+ new RankBadge(accuracy_d, Interpolation.Lerp(accuracy_d, accuracy_c, 0.5), getRank(ScoreRank.D)),
}
},
rankText = new RankText(score.Rank)
@@ -263,7 +277,39 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
using (BeginDelayedSequence(ACCURACY_TRANSFORM_DELAY))
{
- double targetAccuracy = score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH ? 1 : Math.Min(1 - virtual_ss_percentage, score.Accuracy);
+ double targetAccuracy = score.Accuracy;
+ double[] notchPercentages =
+ {
+ accuracy_s,
+ accuracy_a,
+ accuracy_b,
+ accuracy_c,
+ };
+
+ // Ensure the gauge overshoots or undershoots a bit so it doesn't land in the gaps of the inner graded circle (caused by `RankNotch`es),
+ // to prevent ambiguity on what grade it's pointing at.
+ foreach (double p in notchPercentages)
+ {
+ if (Precision.AlmostEquals(p, targetAccuracy, NOTCH_WIDTH_PERCENTAGE / 2))
+ {
+ int tippingDirection = targetAccuracy - p >= 0 ? 1 : -1; // We "round up" here to match rank criteria
+ targetAccuracy = p + tippingDirection * (NOTCH_WIDTH_PERCENTAGE / 2);
+ break;
+ }
+ }
+
+ // The final gap between 99.999...% (S) and 100% (SS) is exaggerated by `virtual_ss_percentage`. We don't want to land there either.
+ if (score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH)
+ targetAccuracy = 1;
+ else
+ targetAccuracy = Math.Min(accuracy_x - virtual_ss_percentage - NOTCH_WIDTH_PERCENTAGE / 2, targetAccuracy);
+
+ // The accuracy circle gauge visually fills up a bit too much.
+ // This wouldn't normally matter but we want it to align properly with the inner graded circle in the above cases.
+ const double visual_alignment_offset = 0.001;
+
+ if (targetAccuracy < 1 && targetAccuracy >= visual_alignment_offset)
+ targetAccuracy -= visual_alignment_offset;
accuracyCircle.FillTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING);
@@ -293,7 +339,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
if (badge.Accuracy > score.Accuracy)
continue;
- using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(1 - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION))
+ using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracy_x - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION))
{
badge.Appear();
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs
index 5432b4cbeb..7af327828e 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs
@@ -27,6 +27,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
///
public readonly double Accuracy;
+ ///
+ /// The position around the to display this badge.
+ ///
+ private readonly double displayPosition;
+
private readonly ScoreRank rank;
private Drawable rankContainer;
@@ -36,10 +41,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// Creates a new .
///
/// The accuracy value corresponding to .
+ /// The position around the to display this badge.
/// The to be displayed in this .
- public RankBadge(double accuracy, ScoreRank rank)
+ public RankBadge(double accuracy, double position, ScoreRank rank)
{
Accuracy = accuracy;
+ displayPosition = position;
this.rank = rank;
RelativeSizeAxes = Axes.Both;
@@ -92,7 +99,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
base.Update();
// Starts at -90deg (top) and moves counter-clockwise by the accuracy
- rankContainer.Position = circlePosition(-MathF.PI / 2 - (1 - (float)Accuracy) * MathF.PI * 2);
+ rankContainer.Position = circlePosition(-MathF.PI / 2 - (1 - (float)displayPosition) * MathF.PI * 2);
}
private Vector2 circlePosition(float t)
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs
index 7e73767318..32f2eb2fa5 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Height = AccuracyCircle.RANK_CIRCLE_RADIUS,
- Width = 1f,
+ Width = (float)AccuracyCircle.NOTCH_WIDTH_PERCENTAGE * 360f,
Colour = OsuColour.Gray(0.3f),
EdgeSmoothness = new Vector2(1f)
}
diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs
new file mode 100644
index 0000000000..b8c9f0b34b
--- /dev/null
+++ b/osu.Game/Screens/Select/FooterV2/FooterButtonModsV2.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+
+namespace osu.Game.Screens.Select.FooterV2
+{
+ public partial class FooterButtonModsV2 : FooterButtonV2
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colour)
+ {
+ Text = "Mods";
+ Icon = FontAwesome.Solid.ExchangeAlt;
+ AccentColour = colour.Lime1;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs
new file mode 100644
index 0000000000..87cca0042a
--- /dev/null
+++ b/osu.Game/Screens/Select/FooterV2/FooterButtonOptionsV2.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Input.Bindings;
+
+namespace osu.Game.Screens.Select.FooterV2
+{
+ public partial class FooterButtonOptionsV2 : FooterButtonV2
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colour)
+ {
+ Text = "Options";
+ Icon = FontAwesome.Solid.Cog;
+ AccentColour = colour.Purple1;
+ Hotkey = GlobalAction.ToggleBeatmapOptions;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonRandomV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonRandomV2.cs
new file mode 100644
index 0000000000..70d1c0c19e
--- /dev/null
+++ b/osu.Game/Screens/Select/FooterV2/FooterButtonRandomV2.cs
@@ -0,0 +1,161 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Input.Bindings;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Screens.Select.FooterV2
+{
+ public partial class FooterButtonRandomV2 : FooterButtonV2
+ {
+ public Action? NextRandom { get; set; }
+ public Action? PreviousRandom { get; set; }
+
+ private Container persistentText = null!;
+ private OsuSpriteText randomSpriteText = null!;
+ private OsuSpriteText rewindSpriteText = null!;
+ private bool rewindSearch;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colour)
+ {
+ //TODO: use https://fontawesome.com/icons/shuffle?s=solid&f=classic when local Fontawesome is updated
+ Icon = FontAwesome.Solid.Random;
+ AccentColour = colour.Blue1;
+ TextContainer.Add(persistentText = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AlwaysPresent = true,
+ AutoSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ randomSpriteText = new OsuSpriteText
+ {
+ Font = OsuFont.TorusAlternate.With(size: 19),
+ AlwaysPresent = true,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = "Random",
+ },
+ rewindSpriteText = new OsuSpriteText
+ {
+ Font = OsuFont.TorusAlternate.With(size: 19),
+ AlwaysPresent = true,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = "Rewind",
+ Alpha = 0f,
+ }
+ }
+ });
+
+ Action = () =>
+ {
+ if (rewindSearch)
+ {
+ const double fade_time = 500;
+
+ OsuSpriteText fallingRewind;
+
+ TextContainer.Add(fallingRewind = new OsuSpriteText
+ {
+ Alpha = 0,
+ Text = rewindSpriteText.Text,
+ AlwaysPresent = true, // make sure the button is sized large enough to always show this
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Font = OsuFont.TorusAlternate.With(size: 19),
+ });
+
+ fallingRewind.FadeOutFromOne(fade_time, Easing.In);
+ fallingRewind.MoveTo(Vector2.Zero).MoveTo(new Vector2(0, 10), fade_time, Easing.In);
+ fallingRewind.Expire();
+
+ persistentText.FadeInFromZero(fade_time, Easing.In);
+
+ PreviousRandom?.Invoke();
+ }
+ else
+ {
+ NextRandom?.Invoke();
+ }
+ };
+ }
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ updateText(e.ShiftPressed);
+ return base.OnKeyDown(e);
+ }
+
+ protected override void OnKeyUp(KeyUpEvent e)
+ {
+ updateText(e.ShiftPressed);
+ base.OnKeyUp(e);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ try
+ {
+ // this uses OR to handle rewinding when clicks are triggered by other sources (i.e. right button in OnMouseUp).
+ rewindSearch |= e.ShiftPressed;
+ return base.OnClick(e);
+ }
+ finally
+ {
+ rewindSearch = false;
+ }
+ }
+
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ if (e.Button == MouseButton.Right)
+ {
+ rewindSearch = true;
+ TriggerClick();
+ return;
+ }
+
+ base.OnMouseUp(e);
+ }
+
+ public override bool OnPressed(KeyBindingPressEvent e)
+ {
+ rewindSearch = e.Action == GlobalAction.SelectPreviousRandom;
+
+ if (e.Action != GlobalAction.SelectNextRandom && e.Action != GlobalAction.SelectPreviousRandom)
+ {
+ return false;
+ }
+
+ if (!e.Repeat)
+ TriggerClick();
+ return true;
+ }
+
+ public override void OnReleased(KeyBindingReleaseEvent e)
+ {
+ if (e.Action == GlobalAction.SelectPreviousRandom)
+ {
+ rewindSearch = false;
+ }
+ }
+
+ private void updateText(bool rewind = false)
+ {
+ randomSpriteText.Alpha = rewind ? 0 : 1;
+ rewindSpriteText.Alpha = rewind ? 1 : 0;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs
new file mode 100644
index 0000000000..2f5046d2bb
--- /dev/null
+++ b/osu.Game/Screens/Select/FooterV2/FooterButtonV2.cs
@@ -0,0 +1,211 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+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.Framework.Graphics.Sprites;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Input.Bindings;
+using osu.Game.Overlays;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Select.FooterV2
+{
+ public partial class FooterButtonV2 : OsuClickableContainer, IKeyBindingHandler
+ {
+ private const int button_height = 90;
+ private const int button_width = 140;
+ private const int corner_radius = 10;
+ private const int transition_length = 500;
+
+ // This should be 12 by design, but an extra allowance is added due to the corner radius specification.
+ public const float SHEAR_WIDTH = 13.5f;
+
+ public Bindable OverlayState = new Bindable();
+
+ protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / button_height, 0);
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ private Colour4 buttonAccentColour;
+
+ protected Colour4 AccentColour
+ {
+ set
+ {
+ buttonAccentColour = value;
+ bar.Colour = buttonAccentColour;
+ icon.Colour = buttonAccentColour;
+ }
+ }
+
+ protected IconUsage Icon
+ {
+ set => icon.Icon = value;
+ }
+
+ protected LocalisableString Text
+ {
+ set => text.Text = value;
+ }
+
+ private readonly SpriteText text;
+ private readonly SpriteIcon icon;
+
+ protected Container TextContainer;
+ private readonly Box bar;
+ private readonly Box backgroundBox;
+
+ public FooterButtonV2()
+ {
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Radius = 4,
+ // Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad.
+ Colour = Colour4.Black.Opacity(0.25f),
+ Offset = new Vector2(0, 2),
+ };
+ Shear = SHEAR;
+ Size = new Vector2(button_width, button_height);
+ Masking = true;
+ CornerRadius = corner_radius;
+ Children = new Drawable[]
+ {
+ backgroundBox = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+
+ // For elements that should not be sheared.
+ new Container
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Shear = -SHEAR,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ TextContainer = new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Y = 42,
+ AutoSizeAxes = Axes.Both,
+ Child = text = new OsuSpriteText
+ {
+ // figma design says the size is 16, but due to the issues with font sizes 19 matches better
+ Font = OsuFont.TorusAlternate.With(size: 19),
+ AlwaysPresent = true
+ }
+ },
+ icon = new SpriteIcon
+ {
+ Y = 12,
+ Size = new Vector2(20),
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre
+ },
+ }
+ },
+ new Container
+ {
+ Shear = -SHEAR,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ Y = -corner_radius,
+ Size = new Vector2(120, 6),
+ Masking = true,
+ CornerRadius = 3,
+ Child = bar = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ OverlayState.BindValueChanged(_ => updateDisplay());
+ Enabled.BindValueChanged(_ => updateDisplay(), true);
+
+ FinishTransforms(true);
+ }
+
+ public GlobalAction? Hotkey;
+
+ private bool handlingMouse;
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateDisplay();
+ return true;
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ handlingMouse = true;
+ updateDisplay();
+ return base.OnMouseDown(e);
+ }
+
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ handlingMouse = false;
+ updateDisplay();
+ base.OnMouseUp(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e) => updateDisplay();
+
+ public virtual bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Action != Hotkey || e.Repeat) return false;
+
+ TriggerClick();
+ return true;
+ }
+
+ public virtual void OnReleased(KeyBindingReleaseEvent e) { }
+
+ private void updateDisplay()
+ {
+ Color4 backgroundColour = colourProvider.Background3;
+
+ if (!Enabled.Value)
+ {
+ backgroundColour = colourProvider.Background3.Darken(0.4f);
+ }
+ else
+ {
+ if (OverlayState.Value == Visibility.Visible)
+ backgroundColour = buttonAccentColour.Darken(0.5f);
+
+ if (IsHovered)
+ {
+ backgroundColour = backgroundColour.Lighten(0.3f);
+
+ if (handlingMouse)
+ backgroundColour = backgroundColour.Lighten(0.3f);
+ }
+ }
+
+ backgroundBox.FadeColour(backgroundColour, transition_length, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Select/FooterV2/FooterV2.cs b/osu.Game/Screens/Select/FooterV2/FooterV2.cs
new file mode 100644
index 0000000000..cd95f3eb6c
--- /dev/null
+++ b/osu.Game/Screens/Select/FooterV2/FooterV2.cs
@@ -0,0 +1,75 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Screens.Select.FooterV2
+{
+ public partial class FooterV2 : InputBlockingContainer
+ {
+ //Should be 60, setting to 50 for now for the sake of matching the current BackButton height.
+ private const int height = 50;
+ private const int padding = 80;
+
+ private readonly List overlays = new List();
+
+ /// The button to be added.
+ /// The to be toggled by this button.
+ public void AddButton(FooterButtonV2 button, OverlayContainer? overlay = null)
+ {
+ if (overlay != null)
+ {
+ overlays.Add(overlay);
+ button.Action = () => showOverlay(overlay);
+ button.OverlayState.BindTo(overlay.State);
+ }
+
+ buttons.Add(button);
+ }
+
+ private void showOverlay(OverlayContainer overlay)
+ {
+ foreach (var o in overlays)
+ {
+ if (o == overlay)
+ o.ToggleVisibility();
+ else
+ o.Hide();
+ }
+ }
+
+ private FillFlowContainer buttons = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = height;
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background5
+ },
+ buttons = new FillFlowContainer
+ {
+ Position = new Vector2(TwoLayerButton.SIZE_EXTENDED.X + padding, 10),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(-FooterButtonV2.SHEAR_WIDTH + 7, 0),
+ AutoSizeAxes = Axes.Both
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
index 0f3f9f2199..a77ea80958 100644
--- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
+++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Tests.Visual
{
///
diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs
index 5a3009dfcd..e74ffc9d54 100644
--- a/osu.Game/Users/Drawables/ClickableAvatar.cs
+++ b/osu.Game/Users/Drawables/ClickableAvatar.cs
@@ -1,11 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
@@ -13,56 +10,49 @@ using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Users.Drawables
{
- public partial class ClickableAvatar : Container
+ public partial class ClickableAvatar : OsuClickableContainer
{
private const string default_tooltip_text = "view profile";
- ///
- /// Whether to open the user's profile when clicked.
- ///
- public bool OpenOnClick
+ public override LocalisableString TooltipText
{
- set => clickableArea.Enabled.Value = clickableArea.Action != null && value;
+ get
+ {
+ if (!Enabled.Value)
+ return string.Empty;
+
+ return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : default_tooltip_text;
+ }
+ set => throw new NotSupportedException();
}
///
/// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username.
/// Setting this to true exposes the username via tooltip for special cases where this is not true.
///
- public bool ShowUsernameTooltip
- {
- set => clickableArea.TooltipText = value ? (user?.Username ?? string.Empty) : default_tooltip_text;
- }
+ public bool ShowUsernameTooltip { get; set; }
- private readonly APIUser user;
+ private readonly APIUser? user;
- [Resolved(CanBeNull = true)]
- private OsuGame game { get; set; }
-
- private readonly ClickableArea clickableArea;
+ [Resolved]
+ private OsuGame? game { get; set; }
///
/// A clickable avatar for the specified user, with UI sounds included.
- /// If is true, clicking will open the user's profile.
///
/// The user. A null value will get a placeholder avatar.
- public ClickableAvatar(APIUser user = null)
+ public ClickableAvatar(APIUser? user = null)
{
this.user = user;
- Add(clickableArea = new ClickableArea
- {
- RelativeSizeAxes = Axes.Both,
- });
-
if (user?.Id != APIUser.SYSTEM_USER_ID)
- clickableArea.Action = openProfile;
+ Action = openProfile;
}
[BackgroundDependencyLoader]
private void load()
{
- LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add);
+ LoadComponentAsync(new DrawableAvatar(user), Add);
}
private void openProfile()
@@ -71,23 +61,12 @@ namespace osu.Game.Users.Drawables
game?.ShowUser(user);
}
- private partial class ClickableArea : OsuClickableContainer
+ protected override bool OnClick(ClickEvent e)
{
- private LocalisableString tooltip = default_tooltip_text;
+ if (!Enabled.Value)
+ return false;
- public override LocalisableString TooltipText
- {
- get => Enabled.Value ? tooltip : default;
- set => tooltip = value;
- }
-
- protected override bool OnClick(ClickEvent e)
- {
- if (!Enabled.Value)
- return false;
-
- return base.OnClick(e);
- }
+ return base.OnClick(e);
}
}
}
diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs
index 9c04eb5706..c659685807 100644
--- a/osu.Game/Users/Drawables/UpdateableAvatar.cs
+++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@@ -13,9 +11,9 @@ namespace osu.Game.Users.Drawables
///
/// An avatar which can update to a new user when needed.
///
- public partial class UpdateableAvatar : ModelBackedDrawable
+ public partial class UpdateableAvatar : ModelBackedDrawable
{
- public APIUser User
+ public APIUser? User
{
get => Model;
set => Model = value;
@@ -58,7 +56,7 @@ namespace osu.Game.Users.Drawables
/// If set to true, hover/click sounds will play and clicking the avatar will open the user's profile.
/// Whether to show the username rather than "view profile" on the tooltip. (note: this only applies if is also true)
/// Whether to show a default guest representation on null user (as opposed to nothing).
- public UpdateableAvatar(APIUser user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
+ public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
{
this.isInteractive = isInteractive;
this.showUsernameTooltip = showUsernameTooltip;
@@ -67,7 +65,7 @@ namespace osu.Game.Users.Drawables
User = user;
}
- protected override Drawable CreateDrawable(APIUser user)
+ protected override Drawable? CreateDrawable(APIUser? user)
{
if (user == null && !showGuestOnNull)
return null;
@@ -76,7 +74,6 @@ namespace osu.Game.Users.Drawables
{
return new ClickableAvatar(user)
{
- OpenOnClick = true,
ShowUsernameTooltip = showUsernameTooltip,
RelativeSizeAxes = Axes.Both,
};
diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs
index 4ea3c036c1..3c1b68f9ef 100644
--- a/osu.Game/Users/ExtendedUserPanel.cs
+++ b/osu.Game/Users/ExtendedUserPanel.cs
@@ -106,7 +106,7 @@ namespace osu.Game.Users
// Set status message based on activity (if we have one) and status is not offline
if (activity != null && !(status is UserStatusOffline))
{
- statusMessage.Text = activity.Status;
+ statusMessage.Text = activity.GetStatus();
statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint);
return;
}
diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs
index 6de797ca3a..0b11d12c46 100644
--- a/osu.Game/Users/UserActivity.cs
+++ b/osu.Game/Users/UserActivity.cs
@@ -7,24 +7,31 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
+using osu.Game.Scoring;
using osuTK.Graphics;
namespace osu.Game.Users
{
public abstract class UserActivity
{
- public abstract string Status { get; }
+ public abstract string GetStatus(bool hideIdentifiableInformation = false);
+
public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker;
- public class Modding : UserActivity
+ public class ModdingBeatmap : EditingBeatmap
{
- public override string Status => "Modding a map";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => "Modding a beatmap";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.PurpleDark;
+
+ public ModdingBeatmap(IBeatmapInfo info)
+ : base(info)
+ {
+ }
}
public class ChoosingBeatmap : UserActivity
{
- public override string Status => "Choosing a beatmap";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => "Choosing a beatmap";
}
public abstract class InGame : UserActivity
@@ -39,7 +46,7 @@ namespace osu.Game.Users
Ruleset = ruleset;
}
- public override string Status => Ruleset.CreateInstance().PlayingVerb;
+ public override string GetStatus(bool hideIdentifiableInformation = false) => Ruleset.CreateInstance().PlayingVerb;
}
public class InMultiplayerGame : InGame
@@ -49,7 +56,7 @@ namespace osu.Game.Users
{
}
- public override string Status => $@"{base.Status} with others";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => $@"{base.GetStatus(hideIdentifiableInformation)} with others";
}
public class SpectatingMultiplayerGame : InGame
@@ -59,7 +66,7 @@ namespace osu.Game.Users
{
}
- public override string Status => $"Watching others {base.Status.ToLowerInvariant()}";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => $"Watching others {base.GetStatus(hideIdentifiableInformation).ToLowerInvariant()}";
}
public class InPlaylistGame : InGame
@@ -78,31 +85,62 @@ namespace osu.Game.Users
}
}
- public class Editing : UserActivity
+ public class TestingBeatmap : InGame
+ {
+ public override string GetStatus(bool hideIdentifiableInformation = false) => "Testing a beatmap";
+
+ public TestingBeatmap(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset)
+ : base(beatmapInfo, ruleset)
+ {
+ }
+ }
+
+ public class EditingBeatmap : UserActivity
{
public IBeatmapInfo BeatmapInfo { get; }
- public Editing(IBeatmapInfo info)
+ public EditingBeatmap(IBeatmapInfo info)
{
BeatmapInfo = info;
}
- public override string Status => @"Editing a beatmap";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => @"Editing a beatmap";
}
- public class Spectating : UserActivity
+ public class WatchingReplay : UserActivity
{
- public override string Status => @"Spectating a game";
+ private readonly ScoreInfo score;
+
+ protected string Username => score.User.Username;
+
+ public BeatmapInfo BeatmapInfo => score.BeatmapInfo;
+
+ public WatchingReplay(ScoreInfo score)
+ {
+ this.score = score;
+ }
+
+ public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Watching a replay" : $@"Watching {Username}'s replay";
+ }
+
+ public class SpectatingUser : WatchingReplay
+ {
+ public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Spectating a user" : $@"Spectating {Username}";
+
+ public SpectatingUser(ScoreInfo score)
+ : base(score)
+ {
+ }
}
public class SearchingForLobby : UserActivity
{
- public override string Status => @"Looking for a lobby";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => @"Looking for a lobby";
}
public class InLobby : UserActivity
{
- public override string Status => @"In a lobby";
+ public override string GetStatus(bool hideIdentifiableInformation = false) => @"In a lobby";
public readonly Room Room;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index cdb3d9b66b..65ea301cbd 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,29 +18,29 @@
-
+
-
-
-
-
-
-
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
-
+