diff --git a/osu.Android.props b/osu.Android.props
index 71525a7acb..1a2859c851 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 5d191119b9..1bf63ef6d4 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -87,8 +87,8 @@ namespace osu.Game.Rulesets.Osu.Mods
requiresHold |= slider.Ball.IsHovered || h.IsHovered;
break;
- case DrawableSpinner _:
- requiresHold = true;
+ case DrawableSpinner spinner:
+ requiresHold |= spinner.HitObject.SpinsRequired > 0;
break;
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
index 0be005e1c4..eec88d7bf8 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Player.ScoreProcessor.NewJudgement += b => judged = true;
});
AddUntilStep("swell judged", () => judged);
- AddAssert("failed", () => Player.HasFailed);
+ AddAssert("failed", () => Player.GameplayState.HasFailed);
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
index 7167d3120a..744227c55e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps()
{
- AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible);
// The pause screen and fail animation both ramp frequency.
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
index fa27e1abdd..6430c29dfa 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps()
{
- AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1);
AddAssert("total number of results == 1", () =>
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 04676f656f..ea0255ab76 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestPauseAfterFail()
{
- AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible);
confirmClockRunning(false);
@@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestExitFromFailedGameplayAfterFailAnimation()
{
- AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible);
confirmClockRunning(false);
@@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestExitFromFailedGameplayDuringFailAnimation()
{
- AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
// will finish the fail animation and show the fail/pause screen.
AddStep("attempt exit via pause key", () => Player.ExitViaPause());
@@ -227,7 +227,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestQuickRetryFromFailedGameplay()
{
- AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke());
confirmExited();
@@ -236,7 +236,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestQuickExitFromFailedGameplay()
{
- AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke());
confirmExited();
@@ -341,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
confirmClockRunning(false);
confirmNotExited();
- AddAssert("player not failed", () => !Player.HasFailed);
+ AddAssert("player not failed", () => !Player.GameplayState.HasFailed);
AddAssert("pause overlay shown", () => Player.PauseOverlayVisible);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
index a9675a2ee2..58b5df2612 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
- AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
@@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay
addFakeHit();
- AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddStep("exit", () => Player.Exit());
AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
index a7b9d45f7a..157c248d69 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
@@ -155,11 +155,13 @@ namespace osu.Game.Tests.Visual.Gameplay
waitForPlayer();
checkPaused(true);
+ sendFrames();
- finish();
+ finish(SpectatedUserState.Failed);
- checkPaused(false);
- // TODO: should replay until running out of frames then fail
+ checkPaused(false); // Should continue playing until out of frames
+ checkPaused(true); // And eventually stop after running out of frames and fail.
+ // Todo: Should check for + display a failed message.
}
[Test]
@@ -211,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("send frames and finish play", () =>
{
spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero));
- spectatorClient.EndPlaying();
+ spectatorClient.EndPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()) { HasPassed = true });
});
// We can't access API because we're an "online" test.
@@ -234,6 +236,71 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("last frame has header", () => lastBundle.Frames[^1].Header != null);
}
+ [Test]
+ public void TestPlayingState()
+ {
+ loadSpectatingScreen();
+
+ start();
+ sendFrames();
+ waitForPlayer();
+ AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
+ }
+
+ [Test]
+ public void TestPassedState()
+ {
+ loadSpectatingScreen();
+
+ start();
+ sendFrames();
+ waitForPlayer();
+
+ AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Passed));
+ AddUntilStep("state is passed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Passed);
+
+ start();
+ sendFrames();
+ waitForPlayer();
+ AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
+ }
+
+ [Test]
+ public void TestQuitState()
+ {
+ loadSpectatingScreen();
+
+ start();
+ sendFrames();
+ waitForPlayer();
+
+ AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id));
+ AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit);
+
+ start();
+ sendFrames();
+ waitForPlayer();
+ AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
+ }
+
+ [Test]
+ public void TestFailedState()
+ {
+ loadSpectatingScreen();
+
+ start();
+ sendFrames();
+ waitForPlayer();
+
+ AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Failed));
+ AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed);
+
+ start();
+ sendFrames();
+ waitForPlayer();
+ AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
+ }
+
private OsuFramedReplayInputHandler replayHandler =>
(OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler;
@@ -246,7 +313,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
- private void finish() => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id));
+ private void finish(SpectatedUserState state = SpectatedUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state));
private void checkPaused(bool state) =>
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs
index 409cec4cf6..034519fbf8 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs
@@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestClientSendsCorrectRuleset()
{
- AddUntilStep("spectator client sending frames", () => spectatorClient.PlayingUserStates.ContainsKey(dummy_user_id));
- AddAssert("spectator client sent correct ruleset", () => spectatorClient.PlayingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID);
+ AddUntilStep("spectator client sending frames", () => spectatorClient.WatchedUserStates.ContainsKey(dummy_user_id));
+ AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchedUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID);
}
public override void TearDownSteps()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index 69798dcb82..b87183cbc7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600);
});
- AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index 9b8e67b07a..1322fbc96e 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void RandomlyUpdateState()
{
- foreach (int userId in PlayingUsers)
+ foreach ((int userId, _) in WatchedUserStates)
{
if (RNG.NextBool())
continue;
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index e31377b96e..8debb95f38 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Navigation
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
- AddUntilStep("wait for fail", () => player.HasFailed);
+ AddUntilStep("wait for fail", () => player.GameplayState.HasFailed);
AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying);
AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime);
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs
index 2883e54385..a68090504d 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs
@@ -3,32 +3,69 @@
using System.IO;
using System.Threading;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Framework.Screens;
+using osu.Game.Overlays;
using osu.Game.Overlays.Settings.Sections.Maintenance;
namespace osu.Game.Tests.Visual.Settings
{
public class TestSceneMigrationScreens : ScreenTestScene
{
+ [Cached]
+ private readonly NotificationOverlay notifications;
+
public TestSceneMigrationScreens()
{
- AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen()));
+ Children = new Drawable[]
+ {
+ notifications = new NotificationOverlay
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ }
+ };
+ }
+
+ [Test]
+ public void TestDeleteSuccess()
+ {
+ AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen(true)));
+ }
+
+ [Test]
+ public void TestDeleteFails()
+ {
+ AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen(false)));
}
private class TestMigrationSelectScreen : MigrationSelectScreen
{
- protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen());
+ private readonly bool deleteSuccess;
+
+ public TestMigrationSelectScreen(bool deleteSuccess)
+ {
+ this.deleteSuccess = deleteSuccess;
+ }
+
+ protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen(deleteSuccess));
private class TestMigrationRunScreen : MigrationRunScreen
{
- protected override void PerformMigration()
- {
- Thread.Sleep(3000);
- }
+ private readonly bool success;
- public TestMigrationRunScreen()
+ public TestMigrationRunScreen(bool success)
: base(null)
{
+ this.success = success;
+ }
+
+ protected override bool PerformMigration()
+ {
+ Thread.Sleep(3000);
+ return success;
}
}
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 4e46901e08..540b820250 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -41,6 +41,68 @@ namespace osu.Game.Tests.Visual.SongSelect
this.rulesets = rulesets;
}
+ [Test]
+ public void TestExternalRulesetChange()
+ {
+ createCarousel(new List());
+
+ AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria
+ {
+ Ruleset = rulesets.AvailableRulesets.ElementAt(0),
+ AllowConvertedBeatmaps = true,
+ }, false));
+
+ AddStep("add mixed ruleset beatmapset", () =>
+ {
+ var testMixed = TestResources.CreateTestBeatmapSetInfo(3);
+
+ for (int i = 0; i <= 2; i++)
+ {
+ testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i);
+ }
+
+ carousel.UpdateBeatmapSet(testMixed);
+ });
+
+ AddUntilStep("wait for filtered difficulties", () =>
+ {
+ var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray();
+
+ return visibleBeatmapPanels.Length == 1
+ && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1;
+ });
+
+ AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria
+ {
+ Ruleset = rulesets.AvailableRulesets.ElementAt(1),
+ AllowConvertedBeatmaps = true,
+ }, false));
+
+ AddUntilStep("wait for filtered difficulties", () =>
+ {
+ var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray();
+
+ return visibleBeatmapPanels.Length == 2
+ && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1
+ && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 1) == 1;
+ });
+
+ AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria
+ {
+ Ruleset = rulesets.AvailableRulesets.ElementAt(2),
+ AllowConvertedBeatmaps = true,
+ }, false));
+
+ AddUntilStep("wait for filtered difficulties", () =>
+ {
+ var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray();
+
+ return visibleBeatmapPanels.Length == 2
+ && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1
+ && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 2) == 1;
+ });
+ }
+
[Test]
public void TestScrollPositionMaintainedOnAdd()
{
diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs
index 347d368a04..b4859d0c91 100644
--- a/osu.Game.Tournament/IO/TournamentStorage.cs
+++ b/osu.Game.Tournament/IO/TournamentStorage.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Tournament.IO
public IEnumerable ListTournaments() => AllTournaments.GetDirectories(string.Empty);
- public override void Migrate(Storage newStorage)
+ public override bool Migrate(Storage newStorage)
{
// this migration only happens once on moving to the per-tournament storage system.
// listed files are those known at that point in time.
@@ -94,6 +94,8 @@ namespace osu.Game.Tournament.IO
ChangeTargetStorage(newStorage);
storageConfig.SetValue(StorageConfig.CurrentTournament, default_tournament);
storageConfig.Save();
+
+ return true;
}
private void moveFileIfExists(string file, DirectoryInfo destination)
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs
index 7753d8480a..eeb86f4702 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
bool firstGroup = true;
- foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key))
+ foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key))
{
if (!firstGroup)
{
diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs
index 5b211084ab..5b467d67e2 100644
--- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs
+++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs
@@ -62,10 +62,8 @@ namespace osu.Game.Beatmaps.Drawables
// matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127
bool collapsed = beatmapSet.Beatmaps.Count() > 12;
- foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key))
- {
- flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key, rulesetGrouping, collapsed));
- }
+ foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key))
+ flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed));
}
protected override void LoadComplete()
diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs
index 1b76725b04..e478144294 100644
--- a/osu.Game/IO/MigratableStorage.cs
+++ b/osu.Game/IO/MigratableStorage.cs
@@ -33,7 +33,8 @@ namespace osu.Game.IO
/// A general purpose migration method to move the storage to a different location.
/// The target storage of the migration.
///
- public virtual void Migrate(Storage newStorage)
+ /// Whether cleanup could complete.
+ public virtual bool Migrate(Storage newStorage)
{
var source = new DirectoryInfo(GetFullPath("."));
var destination = new DirectoryInfo(newStorage.GetFullPath("."));
@@ -57,17 +58,20 @@ namespace osu.Game.IO
CopyRecursive(source, destination);
ChangeTargetStorage(newStorage);
- DeleteRecursive(source);
+
+ return DeleteRecursive(source);
}
- protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true)
+ protected bool DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true)
{
+ bool allFilesDeleted = true;
+
foreach (System.IO.FileInfo fi in target.GetFiles())
{
if (topLevelExcludes && IgnoreFiles.Contains(fi.Name))
continue;
- AttemptOperation(() => fi.Delete());
+ allFilesDeleted &= AttemptOperation(() => fi.Delete(), throwOnFailure: false);
}
foreach (DirectoryInfo dir in target.GetDirectories())
@@ -75,11 +79,13 @@ namespace osu.Game.IO
if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name))
continue;
- AttemptOperation(() => dir.Delete(true));
+ allFilesDeleted &= AttemptOperation(() => dir.Delete(true), throwOnFailure: false);
}
if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0)
- AttemptOperation(target.Delete);
+ allFilesDeleted &= AttemptOperation(target.Delete, throwOnFailure: false);
+
+ return allFilesDeleted;
}
protected void CopyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
@@ -110,19 +116,25 @@ namespace osu.Game.IO
///
/// The action to perform.
/// The number of attempts (250ms wait between each).
- protected static void AttemptOperation(Action action, int attempts = 10)
+ /// Whether to throw an exception on failure. If false, will silently fail.
+ protected static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true)
{
while (true)
{
try
{
action();
- return;
+ return true;
}
catch (Exception)
{
if (attempts-- == 0)
- throw;
+ {
+ if (throwOnFailure)
+ throw;
+
+ return false;
+ }
}
Thread.Sleep(250);
diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs
index 802c71e363..6e7cb545e3 100644
--- a/osu.Game/IO/OsuStorage.cs
+++ b/osu.Game/IO/OsuStorage.cs
@@ -113,11 +113,14 @@ namespace osu.Game.IO
}
}
- public override void Migrate(Storage newStorage)
+ public override bool Migrate(Storage newStorage)
{
- base.Migrate(newStorage);
+ bool cleanupSucceeded = base.Migrate(newStorage);
+
storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath("."));
storageConfig.Save();
+
+ return cleanupSucceeded;
}
}
diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs
index 653abf7427..5d39799f6b 100644
--- a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs
@@ -39,6 +39,7 @@ namespace osu.Game.Online.API.Requests
{
Best,
Firsts,
- Recent
+ Recent,
+ Pinned
}
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
index ebbac0dcab..dca60e54cb 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Online.API.Requests.Responses
public string MD5Hash => Checksum;
- public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID };
+ public IRulesetInfo Ruleset => new APIRuleset { OnlineID = RulesetID };
[JsonIgnore]
public string Hash => throw new NotImplementedException();
@@ -106,5 +106,29 @@ namespace osu.Game.Online.API.Requests.Responses
#endregion
public bool Equals(IBeatmapInfo? other) => other is APIBeatmap b && this.MatchesOnlineID(b);
+
+ private class APIRuleset : IRulesetInfo
+ {
+ public int OnlineID { get; set; } = -1;
+
+ public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})";
+ public string ShortName => nameof(APIRuleset);
+ public string InstantiationInfo => string.Empty;
+
+ public Ruleset CreateInstance() => throw new NotImplementedException();
+
+ public bool Equals(IRulesetInfo? other) => other is APIRuleset r && this.MatchesOnlineID(r);
+
+ public int CompareTo(IRulesetInfo other)
+ {
+ if (!(other is APIRuleset ruleset))
+ throw new ArgumentException($@"Object is not of type {nameof(APIRuleset)}.", nameof(other));
+
+ return OnlineID.CompareTo(ruleset.OnlineID);
+ }
+
+ // ReSharper disable once NonReadonlyMemberInGetHashCode
+ public override int GetHashCode() => OnlineID;
+ }
}
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs
index e4a432b074..2b64e5de06 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUser.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs
@@ -151,6 +151,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"scores_recent_count")]
public int ScoresRecentCount;
+ [JsonProperty(@"scores_pinned_count")]
+ public int ScoresPinnedCount;
+
[JsonProperty(@"beatmap_playcounts_count")]
public int BeatmapPlayCountsCount;
diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
index e24d113822..39193be1af 100644
--- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
+++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
@@ -1,46 +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 System.Net.Http;
-using Newtonsoft.Json;
-using osu.Framework.IO.Network;
-using osu.Game.Online.API;
-using osu.Game.Online.Solo;
using osu.Game.Scoring;
namespace osu.Game.Online.Rooms
{
- public class SubmitRoomScoreRequest : APIRequest
+ public class SubmitRoomScoreRequest : SubmitScoreRequest
{
- private readonly long scoreId;
private readonly long roomId;
private readonly long playlistItemId;
- private readonly SubmittableScore score;
- public SubmitRoomScoreRequest(long scoreId, long roomId, long playlistItemId, ScoreInfo scoreInfo)
+ public SubmitRoomScoreRequest(ScoreInfo scoreInfo, long scoreId, long roomId, long playlistItemId)
+ : base(scoreInfo, scoreId)
{
- this.scoreId = scoreId;
this.roomId = roomId;
this.playlistItemId = playlistItemId;
- score = new SubmittableScore(scoreInfo);
}
- protected override WebRequest CreateWebRequest()
- {
- var req = base.CreateWebRequest();
-
- req.ContentType = "application/json";
- req.Method = HttpMethod.Put;
- req.Timeout = 30000;
-
- req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings
- {
- ReferenceLoopHandling = ReferenceLoopHandling.Ignore
- }));
-
- return req;
- }
-
- protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{scoreId}";
+ protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{ScoreId}";
}
}
diff --git a/osu.Game/Online/Rooms/SubmitScoreRequest.cs b/osu.Game/Online/Rooms/SubmitScoreRequest.cs
new file mode 100644
index 0000000000..b263262d2b
--- /dev/null
+++ b/osu.Game/Online/Rooms/SubmitScoreRequest.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using Newtonsoft.Json;
+using osu.Framework.IO.Network;
+using osu.Game.Online.API;
+using osu.Game.Online.Solo;
+using osu.Game.Scoring;
+
+namespace osu.Game.Online.Rooms
+{
+ public abstract class SubmitScoreRequest : APIRequest
+ {
+ public readonly SubmittableScore Score;
+
+ protected readonly long ScoreId;
+
+ protected SubmitScoreRequest(ScoreInfo scoreInfo, long scoreId)
+ {
+ Score = new SubmittableScore(scoreInfo);
+ ScoreId = scoreId;
+ }
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+
+ req.ContentType = "application/json";
+ req.Method = HttpMethod.Put;
+ req.Timeout = 30000;
+
+ req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings
+ {
+ ReferenceLoopHandling = ReferenceLoopHandling.Ignore
+ }));
+
+ return req;
+ }
+ }
+}
diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs
index 78ebddb2e6..77fd7b813b 100644
--- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs
+++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs
@@ -1,46 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Net.Http;
-using Newtonsoft.Json;
-using osu.Framework.IO.Network;
-using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
namespace osu.Game.Online.Solo
{
- public class SubmitSoloScoreRequest : APIRequest
+ public class SubmitSoloScoreRequest : SubmitScoreRequest
{
- public readonly SubmittableScore Score;
-
- private readonly long scoreId;
-
private readonly int beatmapId;
- public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo)
+ public SubmitSoloScoreRequest(ScoreInfo scoreInfo, long scoreId, int beatmapId)
+ : base(scoreInfo, scoreId)
{
this.beatmapId = beatmapId;
- this.scoreId = scoreId;
- Score = new SubmittableScore(scoreInfo);
}
- protected override WebRequest CreateWebRequest()
- {
- var req = base.CreateWebRequest();
-
- req.ContentType = "application/json";
- req.Method = HttpMethod.Put;
- req.Timeout = 30000;
-
- req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings
- {
- ReferenceLoopHandling = ReferenceLoopHandling.Ignore
- }));
-
- return req;
- }
-
- protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{scoreId}";
+ protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{ScoreId}";
}
}
diff --git a/osu.Game/Online/Spectator/SpectatedUserState.cs b/osu.Game/Online/Spectator/SpectatedUserState.cs
new file mode 100644
index 0000000000..0f0a3068b8
--- /dev/null
+++ b/osu.Game/Online/Spectator/SpectatedUserState.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Online.Spectator
+{
+ public enum SpectatedUserState
+ {
+ ///
+ /// The spectated user is not yet playing.
+ ///
+ Idle,
+
+ ///
+ /// The spectated user is currently playing.
+ ///
+ Playing,
+
+ ///
+ /// The spectated user is currently paused. Unused for the time being.
+ ///
+ Paused,
+
+ ///
+ /// The spectated user has passed gameplay.
+ ///
+ Passed,
+
+ ///
+ /// The spectated user has failed gameplay.
+ ///
+ Failed,
+
+ ///
+ /// The spectated user has quit gameplay.
+ ///
+ Quit
+ }
+}
diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index 67aa75727d..a54ea0d9ee 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -35,19 +35,28 @@ namespace osu.Game.Online.Spectator
///
public abstract IBindable IsConnected { get; }
- private readonly List watchingUsers = new List();
+ ///
+ /// The states of all users currently being watched.
+ ///
+ public IBindableDictionary WatchedUserStates => watchedUserStates;
+ ///
+ /// A global list of all players currently playing.
+ ///
public IBindableList PlayingUsers => playingUsers;
- private readonly BindableList playingUsers = new BindableList();
- public IBindableDictionary PlayingUserStates => playingUserStates;
- private readonly BindableDictionary playingUserStates = new BindableDictionary();
+ ///
+ /// All users currently being watched.
+ ///
+ private readonly List watchedUsers = new List();
+
+ private readonly BindableDictionary watchedUserStates = new BindableDictionary();
+ private readonly BindableList playingUsers = new BindableList();
+ private readonly SpectatorState currentState = new SpectatorState();
private IBeatmap? currentBeatmap;
private Score? currentScore;
- private readonly SpectatorState currentState = new SpectatorState();
-
///
/// Whether the local user is playing.
///
@@ -76,8 +85,8 @@ namespace osu.Game.Online.Spectator
if (connected.NewValue)
{
// get all the users that were previously being watched
- int[] users = watchingUsers.ToArray();
- watchingUsers.Clear();
+ int[] users = watchedUsers.ToArray();
+ watchedUsers.Clear();
// resubscribe to watched users.
foreach (int userId in users)
@@ -90,7 +99,7 @@ namespace osu.Game.Online.Spectator
else
{
playingUsers.Clear();
- playingUserStates.Clear();
+ watchedUserStates.Clear();
}
}), true);
}
@@ -102,11 +111,8 @@ namespace osu.Game.Online.Spectator
if (!playingUsers.Contains(userId))
playingUsers.Add(userId);
- // UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched.
- // This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29).
- // We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations.
- if (watchingUsers.Contains(userId))
- playingUserStates[userId] = state;
+ if (watchedUsers.Contains(userId))
+ watchedUserStates[userId] = state;
OnUserBeganPlaying?.Invoke(userId, state);
});
@@ -119,7 +125,9 @@ namespace osu.Game.Online.Spectator
Schedule(() =>
{
playingUsers.Remove(userId);
- playingUserStates.Remove(userId);
+
+ if (watchedUsers.Contains(userId))
+ watchedUserStates[userId] = state;
OnUserFinishedPlaying?.Invoke(userId, state);
});
@@ -151,6 +159,7 @@ namespace osu.Game.Online.Spectator
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID;
currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
+ currentState.State = SpectatedUserState.Playing;
currentBeatmap = state.Beatmap;
currentScore = score;
@@ -161,7 +170,7 @@ namespace osu.Game.Online.Spectator
public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data);
- public void EndPlaying()
+ public void EndPlaying(GameplayState state)
{
// This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue).
// We probably need to find a better way to handle this...
@@ -176,6 +185,13 @@ namespace osu.Game.Online.Spectator
IsPlaying = false;
currentBeatmap = null;
+ if (state.HasPassed)
+ currentState.State = SpectatedUserState.Passed;
+ else if (state.HasFailed)
+ currentState.State = SpectatedUserState.Failed;
+ else
+ currentState.State = SpectatedUserState.Quit;
+
EndPlayingInternal(currentState);
});
}
@@ -184,10 +200,10 @@ namespace osu.Game.Online.Spectator
{
Debug.Assert(ThreadSafety.IsUpdateThread);
- if (watchingUsers.Contains(userId))
+ if (watchedUsers.Contains(userId))
return;
- watchingUsers.Add(userId);
+ watchedUsers.Add(userId);
WatchUserInternal(userId);
}
@@ -198,8 +214,8 @@ namespace osu.Game.Online.Spectator
// Todo: This should not be a thing, but requires framework changes.
Schedule(() =>
{
- watchingUsers.Remove(userId);
- playingUserStates.Remove(userId);
+ watchedUsers.Remove(userId);
+ watchedUserStates.Remove(userId);
StopWatchingUserInternal(userId);
});
}
diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs
index ebb91e4dd2..77686d12da 100644
--- a/osu.Game/Online/Spectator/SpectatorState.cs
+++ b/osu.Game/Online/Spectator/SpectatorState.cs
@@ -24,14 +24,17 @@ namespace osu.Game.Online.Spectator
[Key(2)]
public IEnumerable Mods { get; set; } = Enumerable.Empty();
+ [Key(3)]
+ public SpectatedUserState State { get; set; }
+
public bool Equals(SpectatorState other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
- return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID;
+ return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID && State == other.State;
}
- public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}";
+ public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID} State:{State}";
}
}
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 5b2eb5607a..0b2644d5ba 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -413,7 +413,7 @@ namespace osu.Game
Scheduler.AddDelayed(GracefullyExit, 2000);
}
- public void Migrate(string path)
+ public bool Migrate(string path)
{
Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""...");
@@ -432,14 +432,15 @@ namespace osu.Game
readyToRun.Wait();
- (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
+ bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
+
+ Logger.Log(@"Migration complete!");
+ return cleanupSucceded != false;
}
finally
{
realmBlocker?.Dispose();
}
-
- Logger.Log(@"Migration complete!");
}
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
index fde20575fc..117de88166 100644
--- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -52,21 +53,24 @@ namespace osu.Game.Overlays.Dashboard
base.LoadComplete();
playingUsers.BindTo(spectatorClient.PlayingUsers);
- playingUsers.BindCollectionChanged(onUsersChanged, true);
+ playingUsers.BindCollectionChanged(onPlayingUsersChanged, true);
}
- private void onUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
+ private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
- foreach (int id in e.NewItems.OfType().ToArray())
+ Debug.Assert(e.NewItems != null);
+
+ foreach (int userId in e.NewItems)
{
- users.GetUserAsync(id).ContinueWith(task =>
+ users.GetUserAsync(userId).ContinueWith(task =>
{
var user = task.GetResultSafely();
- if (user == null) return;
+ if (user == null)
+ return;
Schedule(() =>
{
@@ -82,12 +86,10 @@ namespace osu.Game.Overlays.Dashboard
break;
case NotifyCollectionChangedAction.Remove:
- foreach (int u in e.OldItems.OfType())
- userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire();
- break;
+ Debug.Assert(e.OldItems != null);
- case NotifyCollectionChangedAction.Reset:
- userFlow.Clear();
+ foreach (int userId in e.OldItems)
+ userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire();
break;
}
});
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
index 5532e35cc5..5c67da1911 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
@@ -46,6 +46,9 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
case ScoreType.Recent:
return user.ScoresRecentCount;
+ case ScoreType.Pinned:
+ return user.ScoresPinnedCount;
+
default:
return 0;
}
diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs
index 00a68d5bf9..f48e33dc12 100644
--- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs
+++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs
@@ -18,6 +18,7 @@ namespace osu.Game.Overlays.Profile.Sections
{
Children = new[]
{
+ new PaginatedScoreContainer(ScoreType.Pinned, User, UsersStrings.ShowExtraTopRanksPinnedTitle),
new PaginatedScoreContainer(ScoreType.Best, User, UsersStrings.ShowExtraTopRanksBestTitle),
new PaginatedScoreContainer(ScoreType.Firsts, User, UsersStrings.ShowExtraTopRanksFirstTitle)
};
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs
index b0b61554eb..fb7ff0dbd1 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs
@@ -4,13 +4,16 @@
using System.IO;
using System.Threading.Tasks;
using osu.Framework.Allocation;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
+using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Notifications;
using osu.Game.Screens;
using osuTK;
@@ -23,6 +26,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
+ [Resolved]
+ private NotificationOverlay notifications { get; set; }
+
+ [Resolved]
+ private Storage storage { get; set; }
+
+ [Resolved]
+ private GameHost host { get; set; }
+
public override bool AllowBackButton => false;
public override bool AllowExternalScreenChange => false;
@@ -84,17 +96,33 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Beatmap.Value = Beatmap.Default;
+ var originalStorage = new NativeStorage(storage.GetFullPath(string.Empty), host);
+
migrationTask = Task.Run(PerformMigration)
- .ContinueWith(t =>
+ .ContinueWith(task =>
{
- if (t.IsFaulted)
- Logger.Log($"Error during migration: {t.Exception?.Message}", level: LogLevel.Error);
+ if (task.IsFaulted)
+ {
+ Logger.Error(task.Exception, $"Error during migration: {task.Exception?.Message}");
+ }
+ else if (!task.GetResultSafely())
+ {
+ notifications.Post(new SimpleNotification
+ {
+ Text = "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.",
+ Activated = () =>
+ {
+ originalStorage.PresentExternally();
+ return true;
+ }
+ });
+ }
Schedule(this.Exit);
});
}
- protected virtual void PerformMigration() => game?.Migrate(destination.FullName);
+ protected virtual bool PerformMigration() => game?.Migrate(destination.FullName) != false;
public override void OnEntering(IScreen last)
{
diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs
index ba56adac49..4174aa773c 100644
--- a/osu.Game/Rulesets/EFRulesetInfo.cs
+++ b/osu.Game/Rulesets/EFRulesetInfo.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets
{
[ExcludeFromDynamicCompile]
[Table(@"RulesetInfo")]
- public sealed class EFRulesetInfo : IEquatable, IRulesetInfo
+ public sealed class EFRulesetInfo : IEquatable, IComparable, IRulesetInfo
{
public int? ID { get; set; }
@@ -42,7 +42,15 @@ namespace osu.Game.Rulesets
public bool Equals(EFRulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo;
- public int CompareTo(RulesetInfo other) => OnlineID.CompareTo(other.OnlineID);
+ public int CompareTo(EFRulesetInfo other) => OnlineID.CompareTo(other.OnlineID);
+
+ public int CompareTo(IRulesetInfo other)
+ {
+ if (!(other is EFRulesetInfo ruleset))
+ throw new ArgumentException($@"Object is not of type {nameof(EFRulesetInfo)}.", nameof(other));
+
+ return CompareTo(ruleset);
+ }
public override bool Equals(object obj) => obj is EFRulesetInfo rulesetInfo && Equals(rulesetInfo);
diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs
index 44731a2495..60a02212fc 100644
--- a/osu.Game/Rulesets/IRulesetInfo.cs
+++ b/osu.Game/Rulesets/IRulesetInfo.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets
///
/// A representation of a ruleset's metadata.
///
- public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable
+ public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable
{
///
/// The user-exposed name of this ruleset.
diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs
index 0a0941d1ff..88e3988431 100644
--- a/osu.Game/Rulesets/RulesetInfo.cs
+++ b/osu.Game/Rulesets/RulesetInfo.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets
{
[ExcludeFromDynamicCompile]
[MapTo("Ruleset")]
- public class RulesetInfo : RealmObject, IEquatable, IRulesetInfo
+ public class RulesetInfo : RealmObject, IEquatable, IComparable, IRulesetInfo
{
[PrimaryKey]
public string ShortName { get; set; } = string.Empty;
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets
return ShortName == other.ShortName;
}
- public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b);
+ public bool Equals(IRulesetInfo? other) => other is RulesetInfo r && Equals(r);
public int CompareTo(RulesetInfo other)
{
@@ -63,6 +63,14 @@ namespace osu.Game.Rulesets
return string.Compare(ShortName, other.ShortName, StringComparison.Ordinal);
}
+ public int CompareTo(IRulesetInfo other)
+ {
+ if (!(other is RulesetInfo ruleset))
+ throw new ArgumentException($@"Object is not of type {nameof(RulesetInfo)}.", nameof(other));
+
+ return CompareTo(ruleset);
+ }
+
public override int GetHashCode()
{
// Importantly, ignore the underlying realm hash code, as it will usually not match.
diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs
index 976f95cef8..dcd8f12028 100644
--- a/osu.Game/Rulesets/UI/ReplayRecorder.cs
+++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI
public int RecordFrameRate = 60;
- [Resolved(canBeNull: true)]
+ [Resolved]
private SpectatorClient spectatorClient { get; set; }
[Resolved]
@@ -48,14 +48,13 @@ namespace osu.Game.Rulesets.UI
base.LoadComplete();
inputManager = GetContainingInputManager();
-
- spectatorClient?.BeginPlaying(gameplayState, target);
+ spectatorClient.BeginPlaying(gameplayState, target);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
- spectatorClient?.EndPlaying();
+ spectatorClient?.EndPlaying(gameplayState);
}
protected override void Update()
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 5503a62ba2..2aec63fa65 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -851,7 +851,7 @@ namespace osu.Game.Screens.Edit
var difficultyItems = new List
public readonly Score Score;
+ ///
+ /// Whether gameplay completed without the user failing.
+ ///
+ public bool HasPassed { get; set; }
+
+ ///
+ /// Whether the user failed during gameplay.
+ ///
+ public bool HasFailed { get; set; }
+
+ ///
+ /// Whether the user quit gameplay without having either passed or failed.
+ ///
+ public bool HasQuit { get; set; }
+
///
/// A bindable tracking the last judgement result applied to any hit object.
///
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 240620b686..d4b02622d3 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -72,15 +72,8 @@ namespace osu.Game.Screens.Play
///
protected virtual bool PauseOnFocusLost => true;
- ///
- /// Whether gameplay has completed without the user having failed.
- ///
- public bool GameplayPassed { get; private set; }
-
public Action RestartRequested;
- public bool HasFailed { get; private set; }
-
private Bindable mouseWheelDisabled;
private readonly Bindable storyboardReplacesBackground = new Bindable();
@@ -560,7 +553,7 @@ namespace osu.Game.Screens.Play
if (showDialogFirst && !pauseOrFailDialogVisible)
{
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
- if (ValidForResume && HasFailed)
+ if (ValidForResume && GameplayState.HasFailed)
{
failAnimationLayer.FinishTransforms(true);
return;
@@ -679,7 +672,7 @@ namespace osu.Game.Screens.Play
resultsDisplayDelegate?.Cancel();
resultsDisplayDelegate = null;
- GameplayPassed = false;
+ GameplayState.HasPassed = false;
ValidForResume = true;
skipOutroOverlay.Hide();
return;
@@ -689,7 +682,7 @@ namespace osu.Game.Screens.Play
if (HealthProcessor.HasFailed)
return;
- GameplayPassed = true;
+ GameplayState.HasPassed = true;
// Setting this early in the process means that even if something were to go wrong in the order of events following, there
// is no chance that a user could return to the (already completed) Player instance from a child screen.
@@ -805,7 +798,7 @@ namespace osu.Game.Screens.Play
if (!CheckModsAllowFailure())
return false;
- HasFailed = true;
+ GameplayState.HasFailed = true;
Score.ScoreInfo.Passed = false;
// There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer)
@@ -860,13 +853,13 @@ namespace osu.Game.Screens.Play
// replays cannot be paused and exit immediately
&& !DrawableRuleset.HasReplayLoaded.Value
// cannot pause if we are already in a fail state
- && !HasFailed;
+ && !GameplayState.HasFailed;
private bool canResume =>
// cannot resume from a non-paused state
GameplayClockContainer.IsPaused.Value
// cannot resume if we are already in a fail state
- && !HasFailed
+ && !GameplayState.HasFailed
// already resuming
&& !IsResuming;
@@ -991,6 +984,9 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next)
{
+ if (!GameplayState.HasPassed && !GameplayState.HasFailed)
+ GameplayState.HasQuit = true;
+
screenSuspension?.RemoveAndDisposeImmediately();
failAnimationLayer?.RemoveFilters();
@@ -1005,7 +1001,7 @@ namespace osu.Game.Screens.Play
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
// To resolve test failures, forcefully end playing synchronously when this screen exits.
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
- spectatorClient.EndPlaying();
+ spectatorClient.EndPlaying(GameplayState);
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
// as we are no longer the current screen, we cannot guarantee the track is still usable.
diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
index 1002e7607f..fc96dfa965 100644
--- a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play
protected override APIRequest CreateSubmissionRequest(Score score, long token)
{
Debug.Assert(Room.RoomID.Value != null);
- return new SubmitRoomScoreRequest(token, Room.RoomID.Value.Value, PlaylistItem.ID, score.ScoreInfo);
+ return new SubmitRoomScoreRequest(score.ScoreInfo, token, Room.RoomID.Value.Value, PlaylistItem.ID);
}
}
}
diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
index eced2d142b..824c0072e3 100644
--- a/osu.Game/Screens/Play/SoloPlayer.cs
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play
Debug.Assert(beatmap.OnlineID > 0);
- return new SubmitSoloScoreRequest(beatmap.OnlineID, token, score.ScoreInfo);
+ return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID);
}
}
}
diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs
index b530965269..a0b07fcbd9 100644
--- a/osu.Game/Screens/Play/SoloSpectator.cs
+++ b/osu.Game/Screens/Play/SoloSpectator.cs
@@ -166,7 +166,7 @@ namespace osu.Game.Screens.Play
automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload());
}
- protected override void OnUserStateChanged(int userId, SpectatorState spectatorState)
+ protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
{
clearDisplay();
showBeatmapPanel(spectatorState);
@@ -180,7 +180,7 @@ namespace osu.Game.Screens.Play
scheduleStart(spectatorGameplayState);
}
- protected override void EndGameplay(int userId)
+ protected override void EndGameplay(int userId, SpectatorState state)
{
scheduledStart?.Cancel();
immediateSpectatorGameplayState = null;
diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
index 82523c9d9d..760915b528 100644
--- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
+++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Screens.Select.Carousel
var beatmaps = carouselSet.Beatmaps.ToList();
return beatmaps.Count > maximum_difficulty_icons
- ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset.ShortName)
+ ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset)
.Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset))
: beatmaps.Select(b => new FilterableDifficultyIcon(b));
}
diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs
index 3cf9f79611..9eb374f0f7 100644
--- a/osu.Game/Screens/Spectate/SpectatorScreen.cs
+++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Screens.Spectate
[Resolved]
private UserLookupCache userLookupCache { get; set; }
- private readonly IBindableDictionary playingUserStates = new BindableDictionary();
+ private readonly IBindableDictionary userStates = new BindableDictionary();
private readonly Dictionary userMap = new Dictionary();
private readonly Dictionary gameplayStates = new Dictionary();
@@ -77,8 +77,8 @@ namespace osu.Game.Screens.Spectate
userMap[u.Id] = u;
}
- playingUserStates.BindTo(spectatorClient.PlayingUserStates);
- playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true);
+ userStates.BindTo(spectatorClient.WatchedUserStates);
+ userStates.BindCollectionChanged(onUserStatesChanged, true);
realmSubscription = realm.RegisterForNotifications(
realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged);
@@ -99,51 +99,55 @@ namespace osu.Game.Screens.Spectate
{
foreach ((int userId, _) in userMap)
{
- if (!playingUserStates.TryGetValue(userId, out var userState))
+ if (!userStates.TryGetValue(userId, out var userState))
continue;
if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID))
- updateGameplayState(userId);
+ startGameplay(userId);
}
}
- private void onPlayingUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e)
+ private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e)
{
switch (e.Action)
{
case NotifyDictionaryChangedAction.Add:
+ case NotifyDictionaryChangedAction.Replace:
foreach ((int userId, var state) in e.NewItems.AsNonNull())
- onUserStateAdded(userId, state);
+ onUserStateChanged(userId, state);
break;
case NotifyDictionaryChangedAction.Remove:
- foreach ((int userId, var _) in e.OldItems.AsNonNull())
- onUserStateRemoved(userId);
- break;
-
- case NotifyDictionaryChangedAction.Replace:
- foreach ((int userId, var _) in e.OldItems.AsNonNull())
- onUserStateRemoved(userId);
-
- foreach ((int userId, var state) in e.NewItems.AsNonNull())
- onUserStateAdded(userId, state);
+ foreach ((int userId, SpectatorState state) in e.OldItems.AsNonNull())
+ onUserStateRemoved(userId, state);
break;
}
}
- private void onUserStateAdded(int userId, SpectatorState state)
+ private void onUserStateChanged(int userId, SpectatorState newState)
{
- if (state.RulesetID == null || state.BeatmapID == null)
+ if (newState.RulesetID == null || newState.BeatmapID == null)
return;
if (!userMap.ContainsKey(userId))
return;
- Schedule(() => OnUserStateChanged(userId, state));
- updateGameplayState(userId);
+ switch (newState.State)
+ {
+ case SpectatedUserState.Passed:
+ // Make sure that gameplay completes to the end.
+ if (gameplayStates.TryGetValue(userId, out var gameplayState))
+ gameplayState.Score.Replay.HasReceivedAllFrames = true;
+ break;
+
+ case SpectatedUserState.Playing:
+ Schedule(() => OnNewPlayingUserState(userId, newState));
+ startGameplay(userId);
+ break;
+ }
}
- private void onUserStateRemoved(int userId)
+ private void onUserStateRemoved(int userId, SpectatorState state)
{
if (!userMap.ContainsKey(userId))
return;
@@ -154,15 +158,15 @@ namespace osu.Game.Screens.Spectate
gameplayState.Score.Replay.HasReceivedAllFrames = true;
gameplayStates.Remove(userId);
- Schedule(() => EndGameplay(userId));
+ Schedule(() => EndGameplay(userId, state));
}
- private void updateGameplayState(int userId)
+ private void startGameplay(int userId)
{
Debug.Assert(userMap.ContainsKey(userId));
var user = userMap[userId];
- var spectatorState = playingUserStates[userId];
+ var spectatorState = userStates[userId];
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.OnlineID == spectatorState.RulesetID)?.CreateInstance();
if (resolvedRuleset == null)
@@ -191,11 +195,11 @@ namespace osu.Game.Screens.Spectate
}
///
- /// Invoked when a spectated user's state has changed.
+ /// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing.
///
/// The user whose state has changed.
/// The new state.
- protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState);
+ protected abstract void OnNewPlayingUserState(int userId, [NotNull] SpectatorState spectatorState);
///
/// Starts gameplay for a user.
@@ -208,7 +212,8 @@ namespace osu.Game.Screens.Spectate
/// Ends gameplay for a user.
///
/// The user to end gameplay for.
- protected abstract void EndGameplay(int userId);
+ /// The final user state.
+ protected abstract void EndGameplay(int userId, SpectatorState state);
///
/// Stops spectating a user.
@@ -216,7 +221,10 @@ namespace osu.Game.Screens.Spectate
/// The user to stop spectating.
protected void RemoveUser(int userId)
{
- onUserStateRemoved(userId);
+ if (!userStates.TryGetValue(userId, out var state))
+ return;
+
+ onUserStateRemoved(userId, state);
users.Remove(userId);
userMap.Remove(userId);
diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
index 453c086604..1322a99ea7 100644
--- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
+++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
@@ -58,7 +58,8 @@ namespace osu.Game.Tests.Visual.Spectator
/// Ends play for an arbitrary user.
///
/// The user to end play for.
- public void EndPlay(int userId)
+ /// The spectator state to end play with.
+ public void EndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit)
{
if (!userBeatmapDictionary.ContainsKey(userId))
return;
@@ -67,6 +68,7 @@ namespace osu.Game.Tests.Visual.Spectator
{
BeatmapID = userBeatmapDictionary[userId],
RulesetID = 0,
+ State = state
});
userBeatmapDictionary.Remove(userId);
@@ -142,6 +144,7 @@ namespace osu.Game.Tests.Visual.Spectator
{
BeatmapID = userBeatmapDictionary[userId],
RulesetID = 0,
+ State = SpectatedUserState.Playing
});
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index c8f634284b..a9c0226951 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -37,7 +37,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 7e5ab37257..4a1dc53281 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+