diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index 5ec9e88728..6491987abe 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -8,14 +8,18 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Tests.Resources; @@ -58,14 +62,35 @@ namespace osu.Game.Tests.Visual.Gameplay protected override bool HasCustomSteps => true; - protected override bool AllowFail => false; + protected override bool AllowFail => allowFail; + + private bool allowFail; + + [SetUp] + public void SetUp() + { + allowFail = false; + customRuleset = null; + } + + [Test] + public void TestSaveFailedReplay() + { + AddStep("allow fail", () => allowFail = true); + + CreateTest(); + + AddUntilStep("fail screen displayed", () => Player.ChildrenOfType().First().State.Value == Visibility.Visible); + AddUntilStep("score not in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) == null)); + AddStep("click save button", () => Player.ChildrenOfType().First().ChildrenOfType().First().TriggerClick()); + AddUntilStep("score not in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); + } [Test] public void TestLastPlayedUpdated() { DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed); - AddStep("set no custom ruleset", () => customRuleset = null); AddAssert("last played is null", () => getLastPlayed() == null); CreateTest(); @@ -77,8 +102,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestScoreStoredLocally() { - AddStep("set no custom ruleset", () => customRuleset = null); - CreateTest(); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); diff --git a/osu.Game/Online/DownloadState.cs b/osu.Game/Online/DownloadState.cs index 3d389d45f9..a58c40d16a 100644 --- a/osu.Game/Online/DownloadState.cs +++ b/osu.Game/Online/DownloadState.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.Online { public enum DownloadState diff --git a/osu.Game/Screens/Play/FailOverlay.cs b/osu.Game/Screens/Play/FailOverlay.cs index 1a31b0f462..2af5102369 100644 --- a/osu.Game/Screens/Play/FailOverlay.cs +++ b/osu.Game/Screens/Play/FailOverlay.cs @@ -3,14 +3,25 @@ #nullable disable +using System; +using System.Threading.Tasks; +using osu.Game.Scoring; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Screens.Play { public class FailOverlay : GameplayMenuOverlay { + public Func> SaveReplay; + public override string Header => "failed"; public override string Description => "you're dead, try again?"; @@ -19,6 +30,39 @@ namespace osu.Game.Screens.Play { AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); + // from #10339 maybe this is a better visual effect + Add(new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new SaveFailedScoreButton(SaveReplay) + { + Width = 300 + }, + } + } + } + }); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b4016fc1cf..516364e13a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -267,6 +267,12 @@ namespace osu.Game.Screens.Play }, FailOverlay = new FailOverlay { + SaveReplay = () => + { + Score.ScoreInfo.Passed = false; + Score.ScoreInfo.Rank = ScoreRank.F; + return prepareAndImportScore(); + }, OnRetry = Restart, OnQuit = () => PerformExit(true), }, @@ -720,7 +726,7 @@ namespace osu.Game.Screens.Play if (!Configuration.ShowResults) return; - prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults); + prepareScoreForDisplayTask ??= Task.Run(prepareAndImportScore); bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; @@ -739,7 +745,7 @@ namespace osu.Game.Screens.Play /// Asynchronously run score preparation operations (database import, online submission etc.). /// /// The final score. - private async Task prepareScoreForResults() + private async Task prepareAndImportScore() { var scoreCopy = Score.DeepClone(); @@ -1024,8 +1030,7 @@ namespace osu.Game.Screens.Play if (prepareScoreForDisplayTask == null) { Score.ScoreInfo.Passed = false; - // potentially should be ScoreRank.F instead? this is the best alternative for now. - Score.ScoreInfo.Rank = ScoreRank.D; + Score.ScoreInfo.Rank = ScoreRank.F; } // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 64b4853a67..e82238945b 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -22,12 +22,21 @@ namespace osu.Game.Screens.Play { private readonly Func, Score> createScore; + private readonly bool replayIsFailedScore; + // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) - protected override bool CheckModsAllowFailure() => false; + protected override bool CheckModsAllowFailure() + { + if (!replayIsFailedScore) + return false; + + return base.CheckModsAllowFailure(); + } public ReplayPlayer(Score score, PlayerConfiguration configuration = null) : this((_, _) => score, configuration) { + replayIsFailedScore = score.ScoreInfo.Rank == ScoreRank.F; } public ReplayPlayer(Func, Score> createScore, PlayerConfiguration configuration = null) diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs new file mode 100644 index 0000000000..3f6e741dff --- /dev/null +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -0,0 +1,92 @@ +// 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.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Database; +using osu.Game.Scoring; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; +using osuTK; + +namespace osu.Game.Screens.Play +{ + public class SaveFailedScoreButton : CompositeDrawable + { + private readonly Bindable state = new Bindable(); + + private readonly Func> importFailedScore; + + private ScoreInfo? importedScore; + + private DownloadButton button = null!; + + public SaveFailedScoreButton(Func> importFailedScore) + { + Size = new Vector2(50, 30); + + this.importFailedScore = importFailedScore; + } + + [BackgroundDependencyLoader] + private void load(OsuGame? game, Player? player, RealmAccess realm) + { + InternalChild = button = new DownloadButton + { + RelativeSizeAxes = Axes.Both, + State = { BindTarget = state }, + Action = () => + { + switch (state.Value) + { + case DownloadState.LocallyAvailable: + game?.PresentScore(importedScore, ScorePresentType.Gameplay); + break; + + case DownloadState.NotDownloaded: + state.Value = DownloadState.Importing; + Task.Run(importFailedScore).ContinueWith(t => + { + importedScore = realm.Run(r => r.Find(t.GetResultSafely().ID)?.Detach()); + Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded); + }); + break; + } + } + }; + + if (player != null) + { + importedScore = realm.Run(r => r.Find(player.Score.ScoreInfo.ID)?.Detach()); + if (importedScore != null) + state.Value = DownloadState.LocallyAvailable; + } + + state.BindValueChanged(state => + { + switch (state.NewValue) + { + case DownloadState.LocallyAvailable: + button.TooltipText = @"watch replay"; + button.Enabled.Value = true; + break; + + case DownloadState.Importing: + button.TooltipText = @"importing score"; + button.Enabled.Value = false; + break; + + default: + button.TooltipText = @"save score"; + button.Enabled.Value = true; + break; + } + }, true); + } + } +}