From eef63b41dae7bc1d7916236b0bb2f0f6800cca4c Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 2 Aug 2023 15:01:58 +0900 Subject: [PATCH 01/35] fetch missing beatmap when import score with missing beatmap --- .../Online/TestSceneReplayMissingBeatmap.cs | 96 ++++++++ osu.Game/OsuGame.cs | 2 + osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 15 +- osu.Game/Scoring/ScoreImporter.cs | 21 ++ osu.Game/Scoring/ScoreManager.cs | 7 + .../Import/ReplayMissingBeatmapScreen.cs | 210 ++++++++++++++++++ 6 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs create mode 100644 osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs b/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs new file mode 100644 index 0000000000..a260cc8593 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs @@ -0,0 +1,96 @@ +// 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; +using NUnit.Framework; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Import; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneReplayMissingBeatmap : OsuGameTestScene + { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + [Test] + public void TestSceneMissingBeatmapWithOnlineAvailable() + { + var beatmap = new APIBeatmap + { + OnlineBeatmapSetID = 173612 + }; + + var beatmapset = new APIBeatmapSet + { + OnlineID = 173612, + }; + + setupBeatmapResponse(beatmap, beatmapset); + + AddStep("import score", () => + { + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var importTask = new ImportTask(resourceStream, "replay.osr"); + + Game.ScoreManager.Import(new[] { importTask }); + } + }); + + AddUntilStep("Replay missing screen show", () => Game.ScreenStack.CurrentScreen.GetType() == typeof(ReplayMissingBeatmapScreen)); + } + + [Test] + public void TestSceneMissingBeatmapWithOnlineUnavailable() + { + setupFailedResponse(); + + AddStep("import score", () => + { + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var importTask = new ImportTask(resourceStream, "replay.osr"); + + Game.ScoreManager.Import(new[] { importTask }); + } + }); + + AddUntilStep("Replay missing screen not show", () => Game.ScreenStack.CurrentScreen.GetType() != typeof(ReplayMissingBeatmapScreen)); + } + + private void setupBeatmapResponse(APIBeatmap b, APIBeatmapSet s) + => AddStep("setup response", () => + { + dummyAPI.HandleRequest = request => + { + if (request is GetBeatmapRequest getBeatmapRequest) + { + getBeatmapRequest.TriggerSuccess(b); + return true; + } + + if (request is GetBeatmapSetRequest getBeatmapSetRequest) + { + getBeatmapSetRequest.TriggerSuccess(s); + return true; + } + + return false; + }; + }); + + private void setupFailedResponse() + => AddStep("setup failed response", () => + { + dummyAPI.HandleRequest = request => + { + request.TriggerFailure(new WebException()); + return true; + }; + }); + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1a40bb8e3d..50b4e4f125 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -854,6 +854,8 @@ namespace osu.Game MultiplayerClient.PostNotification = n => Notifications.Post(n); + ScoreManager.Performer = this; + // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 8c00110909..a191220b37 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -7,6 +7,7 @@ using System; using System.Diagnostics; using System.IO; using System.Linq; +using JetBrains.Annotations; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; @@ -61,7 +62,7 @@ namespace osu.Game.Scoring.Legacy workingBeatmap = GetBeatmap(beatmapHash); if (workingBeatmap is DummyWorkingBeatmap) - throw new BeatmapNotFoundException(beatmapHash); + throw new BeatmapNotFoundException(beatmapHash, stream); scoreInfo.User = new APIUser { Username = sr.ReadString() }; @@ -349,9 +350,19 @@ namespace osu.Game.Scoring.Legacy { public string Hash { get; } - public BeatmapNotFoundException(string hash) + [CanBeNull] + public MemoryStream ScoreStream { get; } + + public BeatmapNotFoundException(string hash, [CanBeNull] Stream scoreStream) { Hash = hash; + + if (scoreStream != null) + { + ScoreStream = new MemoryStream(); + scoreStream.Position = 0; + scoreStream.CopyTo(ScoreStream); + } } } } diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 81b9f57bbc..408c83a5b8 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -9,6 +9,7 @@ using System.Threading; using Newtonsoft.Json; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; @@ -19,6 +20,8 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens; +using osu.Game.Screens.Import; using Realms; namespace osu.Game.Scoring @@ -27,6 +30,8 @@ namespace osu.Game.Scoring { public override IEnumerable HandledExtensions => new[] { ".osr" }; + public IPerformFromScreenRunner? Performer { get; set; } + protected override string[] HashableFileTypes => new[] { ".osr" }; private readonly RulesetStore rulesets; @@ -54,12 +59,28 @@ namespace osu.Game.Scoring } catch (LegacyScoreDecoder.BeatmapNotFoundException e) { + onMissingBeatmap(e); Logger.Log($@"Score '{name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database); return null; } } } + private void onMissingBeatmap(LegacyScoreDecoder.BeatmapNotFoundException e) + { + var req = new GetBeatmapRequest(new BeatmapInfo + { + MD5Hash = e.Hash + }); + + req.Success += res => + { + Performer?.PerformFromScreen(screen => screen.Push(new ReplayMissingBeatmapScreen(res, e.ScoreStream))); + }; + + api.Queue(req); + } + public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); protected override void Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 31b5bd8365..9331168ab0 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Online.API; using osu.Game.Scoring.Legacy; +using osu.Game.Screens; namespace osu.Game.Scoring { @@ -30,6 +31,12 @@ namespace osu.Game.Scoring private readonly ScoreImporter scoreImporter; private readonly LegacyScoreExporter scoreExporter; + [CanBeNull] + public IPerformFromScreenRunner Performer + { + set => scoreImporter.Performer = value; + } + public override bool PauseImports { get => base.PauseImports; diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs new file mode 100644 index 0000000000..d7decc0e4e --- /dev/null +++ b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs @@ -0,0 +1,210 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; +using osuTK; +using Realms; + +namespace osu.Game.Screens.Import +{ + [Cached(typeof(IPreviewTrackOwner))] + public partial class ReplayMissingBeatmapScreen : OsuScreen, IPreviewTrackOwner + { + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private IDisposable? realmSubscription; + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + + private Container beatmapPanelContainer = null!; + private ReplayDownloadButton replayDownloadButton = null!; + private SettingsCheckbox automaticDownload = null!; + + private readonly MemoryStream? scoreStream; + private readonly APIBeatmap beatmap; + + private APIBeatmapSet? beatmapSetInfo; + + public ReplayMissingBeatmapScreen(APIBeatmap beatmap, MemoryStream? scoreStream = null) + { + this.beatmap = beatmap; + this.scoreStream = scoreStream; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OsuConfigManager config) + { + InternalChildren = new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = 20, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + Colour = colours.Gray5, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Margin = new MarginPadding(20), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Beatmap info", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] + { + beatmapPanelContainer = new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + automaticDownload = new SettingsCheckbox + { + LabelText = "Automatically download beatmaps", + Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + replayDownloadButton = new ReplayDownloadButton(new ScoreInfo()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + } + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + var onlineBeatmapRequest = new GetBeatmapSetRequest(beatmap.OnlineBeatmapSetID); + + onlineBeatmapRequest.Success += res => + { + beatmapSetInfo = res; + beatmapPanelContainer.Child = new BeatmapCardNormal(res, allowExpansion: false); + checkForAutomaticDownload(res); + }; + + api.Queue(onlineBeatmapRequest); + + realmSubscription = realm.RegisterForNotifications( + realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); + } + + private void checkForAutomaticDownload(APIBeatmapSet beatmap) + { + if (!automaticDownload.Current.Value) + return; + + beatmapDownloader.Download(beatmap); + } + + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) + { + if (changes?.InsertedIndices == null) return; + + if (beatmapSetInfo == null) return; + + if (scoreStream == null) return; + + if (sender.Any(b => b.OnlineID == beatmapSetInfo.OnlineID)) + { + var progressNotification = new ImportProgressNotification(); + var importTask = new ImportTask(scoreStream, "score.osr"); + + scoreManager.Import(progressNotification, new[] { importTask }) + .ContinueWith(s => + { + s.GetResultSafely>>().FirstOrDefault()?.PerformRead(score => + { + Guid scoreid = score.ID; + Scheduler.Add(() => + { + replayDownloadButton.Score.Value = realm.Realm.Find(scoreid) ?? new ScoreInfo(); + }); + }); + }); + + notificationOverlay?.Post(progressNotification); + + realmSubscription?.Dispose(); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + realmSubscription?.Dispose(); + } + } +} From c07a1ec91ec0e087c6c7a437d5d0b684cea1b4e8 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 2 Aug 2023 16:09:37 +0900 Subject: [PATCH 02/35] retrun DefaultBeatmap when beatmapset already delete pending reimplement https://github.com/ppy/osu/pull/22741 --- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 78eed626f2..c06f4da4ae 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) { - if (beatmapInfo?.BeatmapSet == null) + if (beatmapInfo?.BeatmapSet == null || beatmapInfo.BeatmapSet?.DeletePending == true) return DefaultBeatmap; lock (workingCache) From 4c43c9232970c1ac9c4ce2ba4ad4dc54f910e530 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 2 Aug 2023 17:48:24 +0900 Subject: [PATCH 03/35] ensure dispose stream --- osu.Game/Scoring/ScoreImporter.cs | 2 ++ osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 408c83a5b8..ea3bd8f5ae 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -78,6 +78,8 @@ namespace osu.Game.Scoring Performer?.PerformFromScreen(screen => screen.Push(new ReplayMissingBeatmapScreen(res, e.ScoreStream))); }; + req.Failure += _ => e.ScoreStream?.Dispose(); + api.Queue(req); } diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs index d7decc0e4e..a6215165fb 100644 --- a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs +++ b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs @@ -174,16 +174,17 @@ namespace osu.Game.Screens.Import if (beatmapSetInfo == null) return; - if (scoreStream == null) return; + if (scoreStream == null || !scoreStream.CanRead) return; if (sender.Any(b => b.OnlineID == beatmapSetInfo.OnlineID)) { var progressNotification = new ImportProgressNotification(); var importTask = new ImportTask(scoreStream, "score.osr"); - scoreManager.Import(progressNotification, new[] { importTask }) .ContinueWith(s => { + scoreStream.Dispose(); + s.GetResultSafely>>().FirstOrDefault()?.PerformRead(score => { Guid scoreid = score.ID; @@ -205,6 +206,7 @@ namespace osu.Game.Screens.Import base.Dispose(isDisposing); realmSubscription?.Dispose(); + scoreStream?.Dispose(); } } } From 1bdd054bd60a6af5c0816cbbacf6ca629c771b8a Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 2 Aug 2023 18:15:58 +0900 Subject: [PATCH 04/35] extract method --- .../Import/ReplayMissingBeatmapScreen.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs index a6215165fb..5707721bde 100644 --- a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs +++ b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs @@ -64,6 +64,8 @@ namespace osu.Game.Screens.Import public ReplayMissingBeatmapScreen(APIBeatmap beatmap, MemoryStream? scoreStream = null) { this.beatmap = beatmap; + beatmapSetInfo = beatmap.BeatmapSet; + this.scoreStream = scoreStream; } @@ -145,21 +147,32 @@ namespace osu.Game.Screens.Import { base.LoadComplete(); - var onlineBeatmapRequest = new GetBeatmapSetRequest(beatmap.OnlineBeatmapSetID); - - onlineBeatmapRequest.Success += res => + if (beatmapSetInfo == null) { - beatmapSetInfo = res; - beatmapPanelContainer.Child = new BeatmapCardNormal(res, allowExpansion: false); - checkForAutomaticDownload(res); - }; + var onlineBeatmapRequest = new GetBeatmapSetRequest(beatmap.OnlineBeatmapSetID); - api.Queue(onlineBeatmapRequest); + onlineBeatmapRequest.Success += res => + { + beatmapSetInfo = res; + updateStatus(); + }; + api.Queue(onlineBeatmapRequest); + } + updateStatus(); realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); } + private void updateStatus() + { + if (beatmapSetInfo == null) return; + + beatmapPanelContainer.Clear(); + beatmapPanelContainer.Child = new BeatmapCardNormal(beatmapSetInfo, allowExpansion: false); + checkForAutomaticDownload(beatmapSetInfo); + } + private void checkForAutomaticDownload(APIBeatmapSet beatmap) { if (!automaticDownload.Current.Value) From 6637a5e7bc39db1083e73453025ce29b1f69ea7e Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 2 Aug 2023 18:53:27 +0900 Subject: [PATCH 05/35] ensure Performer not null --- osu.Game/Scoring/ScoreImporter.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index ea3bd8f5ae..db4c0aff89 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -68,6 +68,12 @@ namespace osu.Game.Scoring private void onMissingBeatmap(LegacyScoreDecoder.BeatmapNotFoundException e) { + if (Performer == null) + { + e.ScoreStream?.Dispose(); + return; + } + var req = new GetBeatmapRequest(new BeatmapInfo { MD5Hash = e.Hash @@ -75,7 +81,7 @@ namespace osu.Game.Scoring req.Success += res => { - Performer?.PerformFromScreen(screen => screen.Push(new ReplayMissingBeatmapScreen(res, e.ScoreStream))); + Performer.PerformFromScreen(screen => screen.Push(new ReplayMissingBeatmapScreen(res, e.ScoreStream))); }; req.Failure += _ => e.ScoreStream?.Dispose(); From 0e7e36f114006c02c46b18b5cc79703e68293a4d Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 2 Aug 2023 21:53:14 +0900 Subject: [PATCH 06/35] don't passing stream by exception --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 15 ++------------- osu.Game/Scoring/ScoreImporter.cs | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a191220b37..8c00110909 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -7,7 +7,6 @@ using System; using System.Diagnostics; using System.IO; using System.Linq; -using JetBrains.Annotations; using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; @@ -62,7 +61,7 @@ namespace osu.Game.Scoring.Legacy workingBeatmap = GetBeatmap(beatmapHash); if (workingBeatmap is DummyWorkingBeatmap) - throw new BeatmapNotFoundException(beatmapHash, stream); + throw new BeatmapNotFoundException(beatmapHash); scoreInfo.User = new APIUser { Username = sr.ReadString() }; @@ -350,19 +349,9 @@ namespace osu.Game.Scoring.Legacy { public string Hash { get; } - [CanBeNull] - public MemoryStream ScoreStream { get; } - - public BeatmapNotFoundException(string hash, [CanBeNull] Stream scoreStream) + public BeatmapNotFoundException(string hash) { Hash = hash; - - if (scoreStream != null) - { - ScoreStream = new MemoryStream(); - scoreStream.Position = 0; - scoreStream.CopyTo(ScoreStream); - } } } } diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index db4c0aff89..64394800cd 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Threading; using Newtonsoft.Json; @@ -59,19 +60,24 @@ namespace osu.Game.Scoring } catch (LegacyScoreDecoder.BeatmapNotFoundException e) { - onMissingBeatmap(e); + onMissingBeatmap(e, archive, name); Logger.Log($@"Score '{name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database); return null; } } } - private void onMissingBeatmap(LegacyScoreDecoder.BeatmapNotFoundException e) + private void onMissingBeatmap(LegacyScoreDecoder.BeatmapNotFoundException e, ArchiveReader archive, string name) { if (Performer == null) - { - e.ScoreStream?.Dispose(); return; + + var stream = new MemoryStream(); + + // stream will close after exception throw, so fetch the stream again. + using (var scoreStream = archive.GetStream(name)) + { + scoreStream.CopyTo(stream); } var req = new GetBeatmapRequest(new BeatmapInfo @@ -81,10 +87,10 @@ namespace osu.Game.Scoring req.Success += res => { - Performer.PerformFromScreen(screen => screen.Push(new ReplayMissingBeatmapScreen(res, e.ScoreStream))); + Performer.PerformFromScreen(screen => screen.Push(new ReplayMissingBeatmapScreen(res, stream))); }; - req.Failure += _ => e.ScoreStream?.Dispose(); + req.Failure += _ => stream.Dispose(); api.Queue(req); } From 79892865288507e43b12c7b376ec8b7bfddb3ed4 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Fri, 4 Aug 2023 19:57:25 +0900 Subject: [PATCH 07/35] scoreStream never been null --- osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs index 5707721bde..6fabbd00c8 100644 --- a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs +++ b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs @@ -56,12 +56,12 @@ namespace osu.Game.Screens.Import private ReplayDownloadButton replayDownloadButton = null!; private SettingsCheckbox automaticDownload = null!; - private readonly MemoryStream? scoreStream; + private readonly MemoryStream scoreStream; private readonly APIBeatmap beatmap; private APIBeatmapSet? beatmapSetInfo; - public ReplayMissingBeatmapScreen(APIBeatmap beatmap, MemoryStream? scoreStream = null) + public ReplayMissingBeatmapScreen(APIBeatmap beatmap, MemoryStream scoreStream) { this.beatmap = beatmap; beatmapSetInfo = beatmap.BeatmapSet; @@ -187,7 +187,7 @@ namespace osu.Game.Screens.Import if (beatmapSetInfo == null) return; - if (scoreStream == null || !scoreStream.CanRead) return; + if (!scoreStream.CanRead) return; if (sender.Any(b => b.OnlineID == beatmapSetInfo.OnlineID)) { From 09047538c70629861a2121af5dbe9db04224886d Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Fri, 4 Aug 2023 20:02:22 +0900 Subject: [PATCH 08/35] remove all memory stream dispose --- osu.Game/Scoring/ScoreImporter.cs | 2 -- osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs | 3 --- 2 files changed, 5 deletions(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 64394800cd..5c354ac3d1 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -90,8 +90,6 @@ namespace osu.Game.Scoring Performer.PerformFromScreen(screen => screen.Push(new ReplayMissingBeatmapScreen(res, stream))); }; - req.Failure += _ => stream.Dispose(); - api.Queue(req); } diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs index 6fabbd00c8..2e0e7a8e3f 100644 --- a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs +++ b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs @@ -196,8 +196,6 @@ namespace osu.Game.Screens.Import scoreManager.Import(progressNotification, new[] { importTask }) .ContinueWith(s => { - scoreStream.Dispose(); - s.GetResultSafely>>().FirstOrDefault()?.PerformRead(score => { Guid scoreid = score.ID; @@ -219,7 +217,6 @@ namespace osu.Game.Screens.Import base.Dispose(isDisposing); realmSubscription?.Dispose(); - scoreStream?.Dispose(); } } } From e7b34cd4da77e9b1de5efd950f5351a3d35b6794 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 14 Aug 2023 03:43:16 +0900 Subject: [PATCH 09/35] declare BeatmapSet is not null --- .../Online/TestSceneReplayMissingBeatmap.cs | 21 +++++++------------ .../Import/ReplayMissingBeatmapScreen.cs | 21 ++----------------- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs b/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs index a260cc8593..eb84d80051 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs @@ -21,15 +21,14 @@ namespace osu.Game.Tests.Visual.Online { var beatmap = new APIBeatmap { - OnlineBeatmapSetID = 173612 + OnlineBeatmapSetID = 173612, + BeatmapSet = new APIBeatmapSet + { + OnlineID = 173612 + } }; - var beatmapset = new APIBeatmapSet - { - OnlineID = 173612, - }; - - setupBeatmapResponse(beatmap, beatmapset); + setupBeatmapResponse(beatmap); AddStep("import score", () => { @@ -62,7 +61,7 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("Replay missing screen not show", () => Game.ScreenStack.CurrentScreen.GetType() != typeof(ReplayMissingBeatmapScreen)); } - private void setupBeatmapResponse(APIBeatmap b, APIBeatmapSet s) + private void setupBeatmapResponse(APIBeatmap b) => AddStep("setup response", () => { dummyAPI.HandleRequest = request => @@ -73,12 +72,6 @@ namespace osu.Game.Tests.Visual.Online return true; } - if (request is GetBeatmapSetRequest getBeatmapSetRequest) - { - getBeatmapSetRequest.TriggerSuccess(s); - return true; - } - return false; }; }); diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs index 2e0e7a8e3f..ffd6942b9b 100644 --- a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs +++ b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs @@ -18,7 +18,6 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Settings; @@ -57,14 +56,12 @@ namespace osu.Game.Screens.Import private SettingsCheckbox automaticDownload = null!; private readonly MemoryStream scoreStream; - private readonly APIBeatmap beatmap; - private APIBeatmapSet? beatmapSetInfo; + private readonly APIBeatmapSet beatmapSetInfo; public ReplayMissingBeatmapScreen(APIBeatmap beatmap, MemoryStream scoreStream) { - this.beatmap = beatmap; - beatmapSetInfo = beatmap.BeatmapSet; + beatmapSetInfo = beatmap.BeatmapSet!; this.scoreStream = scoreStream; } @@ -147,18 +144,6 @@ namespace osu.Game.Screens.Import { base.LoadComplete(); - if (beatmapSetInfo == null) - { - var onlineBeatmapRequest = new GetBeatmapSetRequest(beatmap.OnlineBeatmapSetID); - - onlineBeatmapRequest.Success += res => - { - beatmapSetInfo = res; - updateStatus(); - }; - api.Queue(onlineBeatmapRequest); - } - updateStatus(); realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); @@ -185,8 +170,6 @@ namespace osu.Game.Screens.Import { if (changes?.InsertedIndices == null) return; - if (beatmapSetInfo == null) return; - if (!scoreStream.CanRead) return; if (sender.Any(b => b.OnlineID == beatmapSetInfo.OnlineID)) From 3a3951ebf414aa449319b159e13081e42da60bdd Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 17 Aug 2023 17:26:00 +0900 Subject: [PATCH 10/35] remove useless api resolved --- osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs index ffd6942b9b..4e1355b52f 100644 --- a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs +++ b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs @@ -34,9 +34,6 @@ namespace osu.Game.Screens.Import [Resolved] private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - [Resolved] private ScoreManager scoreManager { get; set; } = null!; From 61a1460f093b2d5f2af7c6f4252140718119c0bc Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 17 Aug 2023 17:27:14 +0900 Subject: [PATCH 11/35] remove useless Using --- osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs index 4e1355b52f..3566b50421 100644 --- a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs +++ b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs @@ -17,7 +17,6 @@ using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Settings; From 0db82e5286dc301f5b86cab1c19ae639b8e0d0d9 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 17 Aug 2023 17:27:50 +0900 Subject: [PATCH 12/35] beatmapSetInfo never be null --- osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs index 3566b50421..614d652f47 100644 --- a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs +++ b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs @@ -147,8 +147,6 @@ namespace osu.Game.Screens.Import private void updateStatus() { - if (beatmapSetInfo == null) return; - beatmapPanelContainer.Clear(); beatmapPanelContainer.Child = new BeatmapCardNormal(beatmapSetInfo, allowExpansion: false); checkForAutomaticDownload(beatmapSetInfo); From 58844092d6984f96e5b7f1fd263d675979c8b879 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 16:17:21 +0900 Subject: [PATCH 13/35] post a notification instead a screen --- .../Online/TestSceneReplayMissingBeatmap.cs | 14 +- .../Database/MissingBeatmapNotification.cs | 157 ++++++++++++++ osu.Game/OsuGame.cs | 2 - osu.Game/Scoring/ScoreImporter.cs | 10 +- osu.Game/Scoring/ScoreManager.cs | 7 - .../Import/ReplayMissingBeatmapScreen.cs | 199 ------------------ 6 files changed, 169 insertions(+), 220 deletions(-) create mode 100644 osu.Game/Database/MissingBeatmapNotification.cs delete mode 100644 osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs b/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs index eb84d80051..60197e0eb7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneReplayMissingBeatmap.cs @@ -1,13 +1,15 @@ // 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 System.Net; using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Screens.Import; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Online @@ -24,6 +26,12 @@ namespace osu.Game.Tests.Visual.Online OnlineBeatmapSetID = 173612, BeatmapSet = new APIBeatmapSet { + Title = "FREEDOM Dive", + Artist = "xi", + Covers = new BeatmapSetOnlineCovers + { + Card = "https://assets.ppy.sh/beatmaps/173612/covers/card@2x.jpg" + }, OnlineID = 173612 } }; @@ -40,7 +48,7 @@ namespace osu.Game.Tests.Visual.Online } }); - AddUntilStep("Replay missing screen show", () => Game.ScreenStack.CurrentScreen.GetType() == typeof(ReplayMissingBeatmapScreen)); + AddUntilStep("Replay missing notification show", () => Game.Notifications.ChildrenOfType().Any()); } [Test] @@ -58,7 +66,7 @@ namespace osu.Game.Tests.Visual.Online } }); - AddUntilStep("Replay missing screen not show", () => Game.ScreenStack.CurrentScreen.GetType() != typeof(ReplayMissingBeatmapScreen)); + AddUntilStep("Replay missing notification not show", () => !Game.Notifications.ChildrenOfType().Any()); } private void setupBeatmapResponse(APIBeatmap b) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs new file mode 100644 index 0000000000..2587160a57 --- /dev/null +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -0,0 +1,157 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Scoring; +using osuTK.Graphics; + +namespace osu.Game.Database +{ + public partial class MissingBeatmapNotification : ProgressNotification + { + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + [Resolved] + private BeatmapSetOverlay? beatmapSetOverlay { get; set; } + + private Container beatmapPanelContainer = null!; + + private readonly MemoryStream scoreStream; + + private readonly APIBeatmapSet beatmapSetInfo; + + private BeatmapDownloadTracker? downloadTracker; + + private Bindable autodownloadConfig = null!; + + public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream) + { + beatmapSetInfo = beatmap.BeatmapSet!; + + this.scoreStream = scoreStream; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OsuConfigManager config) + { + autodownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating); + + Text = "You do not have the required beatmap for this replay"; + + Content.Add(beatmapPanelContainer = new ClickableContainer + { + RelativeSizeAxes = Axes.X, + Height = 70, + Anchor = Anchor.CentreLeft, + Origin = Anchor.TopLeft, + Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSetInfo.OnlineID) + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + downloadTracker = new BeatmapDownloadTracker(beatmapSetInfo); + downloadTracker.State.BindValueChanged(downloadStatusChanged, true); + + beatmapPanelContainer.Clear(); + beatmapPanelContainer.Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 4, + Children = new Drawable[] + { + downloadTracker, + new DelayedLoadWrapper(() => new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card) + { + OnlineInfo = beatmapSetInfo, + RelativeSizeAxes = Axes.Both + }) + { + RelativeSizeAxes = Axes.Both + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.4f + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding + { + Left = 10f, + Top = 5f + }, + Children = new Drawable[] + { + new TruncatingSpriteText + { + Text = beatmapSetInfo.Title, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + RelativeSizeAxes = Axes.X, + }, + new TruncatingSpriteText + { + Text = beatmapSetInfo.Artist, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12, italics: true), + RelativeSizeAxes = Axes.X, + } + } + }, + new DownloadButton + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Width = 50, + Height = 30, + Margin = new MarginPadding + { + Bottom = 1f + }, + Action = () => beatmapDownloader.Download(beatmapSetInfo), + State = { BindTarget = downloadTracker.State } + } + } + }; + + if (autodownloadConfig.Value) + beatmapDownloader.Download(beatmapSetInfo); + } + + private void downloadStatusChanged(ValueChangedEvent status) + { + if (status.NewValue != DownloadState.LocallyAvailable) + return; + + var importTask = new ImportTask(scoreStream, "score.osr"); + scoreManager.Import(this, new[] { importTask }); + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5d130af6d4..c60bff9e4c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -854,8 +854,6 @@ namespace osu.Game MultiplayerClient.PostNotification = n => Notifications.Post(n); - ScoreManager.Performer = this; - // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 5c354ac3d1..e3fce4a82a 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -10,7 +10,6 @@ using System.Threading; using Newtonsoft.Json; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; @@ -21,8 +20,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens; -using osu.Game.Screens.Import; using Realms; namespace osu.Game.Scoring @@ -31,8 +28,6 @@ namespace osu.Game.Scoring { public override IEnumerable HandledExtensions => new[] { ".osr" }; - public IPerformFromScreenRunner? Performer { get; set; } - protected override string[] HashableFileTypes => new[] { ".osr" }; private readonly RulesetStore rulesets; @@ -69,9 +64,6 @@ namespace osu.Game.Scoring private void onMissingBeatmap(LegacyScoreDecoder.BeatmapNotFoundException e, ArchiveReader archive, string name) { - if (Performer == null) - return; - var stream = new MemoryStream(); // stream will close after exception throw, so fetch the stream again. @@ -87,7 +79,7 @@ namespace osu.Game.Scoring req.Success += res => { - Performer.PerformFromScreen(screen => screen.Push(new ReplayMissingBeatmapScreen(res, stream))); + PostNotification?.Invoke(new MissingBeatmapNotification(res, stream)); }; api.Queue(req); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 9331168ab0..31b5bd8365 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -21,7 +21,6 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Online.API; using osu.Game.Scoring.Legacy; -using osu.Game.Screens; namespace osu.Game.Scoring { @@ -31,12 +30,6 @@ namespace osu.Game.Scoring private readonly ScoreImporter scoreImporter; private readonly LegacyScoreExporter scoreExporter; - [CanBeNull] - public IPerformFromScreenRunner Performer - { - set => scoreImporter.Performer = value; - } - public override bool PauseImports { get => base.PauseImports; diff --git a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs b/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs deleted file mode 100644 index 614d652f47..0000000000 --- a/osu.Game/Screens/Import/ReplayMissingBeatmapScreen.cs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Audio; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables.Cards; -using osu.Game.Configuration; -using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; -using osu.Game.Overlays.Settings; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking; -using osuTK; -using Realms; - -namespace osu.Game.Screens.Import -{ - [Cached(typeof(IPreviewTrackOwner))] - public partial class ReplayMissingBeatmapScreen : OsuScreen, IPreviewTrackOwner - { - [Resolved] - private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; - - [Resolved] - private ScoreManager scoreManager { get; set; } = null!; - - [Resolved] - private RealmAccess realm { get; set; } = null!; - - private IDisposable? realmSubscription; - - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - - [Resolved] - private INotificationOverlay? notificationOverlay { get; set; } - - private Container beatmapPanelContainer = null!; - private ReplayDownloadButton replayDownloadButton = null!; - private SettingsCheckbox automaticDownload = null!; - - private readonly MemoryStream scoreStream; - - private readonly APIBeatmapSet beatmapSetInfo; - - public ReplayMissingBeatmapScreen(APIBeatmap beatmap, MemoryStream scoreStream) - { - beatmapSetInfo = beatmap.BeatmapSet!; - - this.scoreStream = scoreStream; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager config) - { - InternalChildren = new Drawable[] - { - new Container - { - Masking = true, - CornerRadius = 20, - AutoSizeAxes = Axes.Both, - AutoSizeDuration = 500, - AutoSizeEasing = Easing.OutQuint, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - Colour = colours.Gray5, - RelativeSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - Margin = new MarginPadding(20), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(15), - Children = new Drawable[] - { - new OsuSpriteText - { - Text = "Beatmap info", - Font = OsuFont.Default.With(size: 30), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(15), - Children = new Drawable[] - { - beatmapPanelContainer = new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - }, - automaticDownload = new SettingsCheckbox - { - LabelText = "Automatically download beatmaps", - Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - replayDownloadButton = new ReplayDownloadButton(new ScoreInfo()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } - } - } - }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateStatus(); - realmSubscription = realm.RegisterForNotifications( - realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); - } - - private void updateStatus() - { - beatmapPanelContainer.Clear(); - beatmapPanelContainer.Child = new BeatmapCardNormal(beatmapSetInfo, allowExpansion: false); - checkForAutomaticDownload(beatmapSetInfo); - } - - private void checkForAutomaticDownload(APIBeatmapSet beatmap) - { - if (!automaticDownload.Current.Value) - return; - - beatmapDownloader.Download(beatmap); - } - - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) - { - if (changes?.InsertedIndices == null) return; - - if (!scoreStream.CanRead) return; - - if (sender.Any(b => b.OnlineID == beatmapSetInfo.OnlineID)) - { - var progressNotification = new ImportProgressNotification(); - var importTask = new ImportTask(scoreStream, "score.osr"); - scoreManager.Import(progressNotification, new[] { importTask }) - .ContinueWith(s => - { - s.GetResultSafely>>().FirstOrDefault()?.PerformRead(score => - { - Guid scoreid = score.ID; - Scheduler.Add(() => - { - replayDownloadButton.Score.Value = realm.Realm.Find(scoreid) ?? new ScoreInfo(); - }); - }); - }); - - notificationOverlay?.Post(progressNotification); - - realmSubscription?.Dispose(); - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - realmSubscription?.Dispose(); - } - } -} From 3decadaf519c26a6c4b941d065d41ce441589821 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 16:18:14 +0900 Subject: [PATCH 14/35] use realm query --- osu.Game/Beatmaps/BeatmapManager.cs | 3 ++- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d71d7b7f67..1f551f1218 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -26,6 +26,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Utils; +using Realms; namespace osu.Game.Beatmaps { @@ -284,7 +285,7 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapInfo? QueryBeatmap(Expression> query) => Realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); + public BeatmapInfo? QueryBeatmap(Expression> query) => Realm.Run(r => r.All().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach()); /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index c06f4da4ae..78eed626f2 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) { - if (beatmapInfo?.BeatmapSet == null || beatmapInfo.BeatmapSet?.DeletePending == true) + if (beatmapInfo?.BeatmapSet == null) return DefaultBeatmap; lock (workingCache) From 164f61f59034f2d713cdb4676f7327a4f41b512f Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 17:14:04 +0900 Subject: [PATCH 15/35] clean up --- .../Database/MissingBeatmapNotification.cs | 53 +++++++------------ 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 2587160a57..7a39c6307b 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; @@ -30,21 +31,12 @@ namespace osu.Game.Database [Resolved] private ScoreManager scoreManager { get; set; } = null!; - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - - [Resolved] - private BeatmapSetOverlay? beatmapSetOverlay { get; set; } - - private Container beatmapPanelContainer = null!; - private readonly MemoryStream scoreStream; private readonly APIBeatmapSet beatmapSetInfo; - private BeatmapDownloadTracker? downloadTracker; - private Bindable autodownloadConfig = null!; + private Bindable noVideoSetting = null!; public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream) { @@ -54,35 +46,25 @@ namespace osu.Game.Database } [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager config) + private void load(OsuConfigManager config, BeatmapSetOverlay? beatmapSetOverlay) { + BeatmapDownloadTracker downloadTracker = new BeatmapDownloadTracker(beatmapSetInfo); + downloadTracker.State.BindValueChanged(downloadStatusChanged); + autodownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating); + noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); Text = "You do not have the required beatmap for this replay"; - Content.Add(beatmapPanelContainer = new ClickableContainer + Content.Add(new ClickableContainer { RelativeSizeAxes = Axes.X, Height = 70, Anchor = Anchor.CentreLeft, Origin = Anchor.TopLeft, - Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSetInfo.OnlineID) - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - downloadTracker = new BeatmapDownloadTracker(beatmapSetInfo); - downloadTracker.State.BindValueChanged(downloadStatusChanged, true); - - beatmapPanelContainer.Clear(); - beatmapPanelContainer.Child = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, CornerRadius = 4, + Masking = true, + Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSetInfo.OnlineID), Children = new Drawable[] { downloadTracker, @@ -125,7 +107,7 @@ namespace osu.Game.Database } } }, - new DownloadButton + new BeatmapDownloadButton(beatmapSetInfo) { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, @@ -134,15 +116,18 @@ namespace osu.Game.Database Margin = new MarginPadding { Bottom = 1f - }, - Action = () => beatmapDownloader.Download(beatmapSetInfo), - State = { BindTarget = downloadTracker.State } + } } } - }; + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); if (autodownloadConfig.Value) - beatmapDownloader.Download(beatmapSetInfo); + beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value); } private void downloadStatusChanged(ValueChangedEvent status) From f68a12003a6df0dc32d9eefc391ec5228d1e9ddf Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 17:37:31 +0900 Subject: [PATCH 16/35] check beatmap hash before try to import --- .../Database/MissingBeatmapNotification.cs | 33 ++++++++++++++----- osu.Game/Scoring/ScoreImporter.cs | 2 +- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 7a39c6307b..92b33e20be 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -2,18 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; +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.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; @@ -31,31 +30,43 @@ namespace osu.Game.Database [Resolved] private ScoreManager scoreManager { get; set; } = null!; - private readonly MemoryStream scoreStream; + [Resolved] + private RealmAccess realm { get; set; } = null!; + private readonly MemoryStream scoreStream; private readonly APIBeatmapSet beatmapSetInfo; + private readonly string beatmapHash; private Bindable autodownloadConfig = null!; private Bindable noVideoSetting = null!; - public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream) + public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream, string beatmapHash) { beatmapSetInfo = beatmap.BeatmapSet!; + this.beatmapHash = beatmapHash; this.scoreStream = scoreStream; } [BackgroundDependencyLoader] private void load(OsuConfigManager config, BeatmapSetOverlay? beatmapSetOverlay) { + Text = "You do not have the required beatmap for this replay"; + + realm.Run(r => + { + if (r.All().Any(s => s.OnlineID == beatmapSetInfo.OnlineID)) + { + Text = "You have the corresponding beatmapset but no beatmap, you may need to update the beatmap."; + } + }); + BeatmapDownloadTracker downloadTracker = new BeatmapDownloadTracker(beatmapSetInfo); downloadTracker.State.BindValueChanged(downloadStatusChanged); autodownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating); noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); - Text = "You do not have the required beatmap for this replay"; - Content.Add(new ClickableContainer { RelativeSizeAxes = Axes.X, @@ -135,8 +146,14 @@ namespace osu.Game.Database if (status.NewValue != DownloadState.LocallyAvailable) return; - var importTask = new ImportTask(scoreStream, "score.osr"); - scoreManager.Import(this, new[] { importTask }); + realm.Run(r => + { + if (r.All().Any(s => s.MD5Hash == beatmapHash)) + { + var importTask = new ImportTask(scoreStream, "score.osr"); + scoreManager.Import(this, new[] { importTask }); + } + }); } } } diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index e3fce4a82a..650e25a512 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -79,7 +79,7 @@ namespace osu.Game.Scoring req.Success += res => { - PostNotification?.Invoke(new MissingBeatmapNotification(res, stream)); + PostNotification?.Invoke(new MissingBeatmapNotification(res, stream, e.Hash)); }; api.Queue(req); From 87aa191c121214c18e0a4270e7f20f856da40ab2 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 17:53:12 +0900 Subject: [PATCH 17/35] use realm Subscription instead of Beatmap Download Tracker --- .../Database/MissingBeatmapNotification.cs | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 92b33e20be..86522d0864 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.IO; using System.Linq; using osu.Framework.Allocation; @@ -13,12 +14,12 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Scoring; using osuTK.Graphics; +using Realms; namespace osu.Game.Database { @@ -40,6 +41,8 @@ namespace osu.Game.Database private Bindable autodownloadConfig = null!; private Bindable noVideoSetting = null!; + private IDisposable? realmSubscription; + public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream, string beatmapHash) { beatmapSetInfo = beatmap.BeatmapSet!; @@ -53,17 +56,17 @@ namespace osu.Game.Database { Text = "You do not have the required beatmap for this replay"; + realmSubscription = realm.RegisterForNotifications( + realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); + realm.Run(r => { - if (r.All().Any(s => s.OnlineID == beatmapSetInfo.OnlineID)) + if (r.All().Any(s => !s.DeletePending && s.OnlineID == beatmapSetInfo.OnlineID)) { - Text = "You have the corresponding beatmapset but no beatmap, you may need to update the beatmap."; + Text = "You have the corresponding beatmapset but no beatmap, you may need to update the beatmapset."; } }); - BeatmapDownloadTracker downloadTracker = new BeatmapDownloadTracker(beatmapSetInfo); - downloadTracker.State.BindValueChanged(downloadStatusChanged); - autodownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating); noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); @@ -78,7 +81,6 @@ namespace osu.Game.Database Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSetInfo.OnlineID), Children = new Drawable[] { - downloadTracker, new DelayedLoadWrapper(() => new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card) { OnlineInfo = beatmapSetInfo, @@ -141,19 +143,16 @@ namespace osu.Game.Database beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value); } - private void downloadStatusChanged(ValueChangedEvent status) + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) { - if (status.NewValue != DownloadState.LocallyAvailable) - return; + if (changes?.InsertedIndices == null) return; - realm.Run(r => + if (sender.Any(s => s.Beatmaps.Any(b => b.MD5Hash == beatmapHash))) { - if (r.All().Any(s => s.MD5Hash == beatmapHash)) - { - var importTask = new ImportTask(scoreStream, "score.osr"); - scoreManager.Import(this, new[] { importTask }); - } - }); + var importTask = new ImportTask(scoreStream, "score.osr"); + scoreManager.Import(this, new[] { importTask }); + realmSubscription?.Dispose(); + } } } } From fd1fce486a18c6b12859b3fa197c707bac583751 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Tue, 5 Sep 2023 00:21:08 +0900 Subject: [PATCH 18/35] ensure dispose realmSubscription --- osu.Game/Database/MissingBeatmapNotification.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 86522d0864..d6674b9434 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -154,5 +154,11 @@ namespace osu.Game.Database realmSubscription?.Dispose(); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + realmSubscription?.Dispose(); + } } } From 59b9a636d372b4167ce2475376bc31394c30fb2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 10:46:14 +0200 Subject: [PATCH 19/35] Fix grammar in comment --- osu.Game/Scoring/ScoreImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 650e25a512..2875035e1b 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -66,7 +66,7 @@ namespace osu.Game.Scoring { var stream = new MemoryStream(); - // stream will close after exception throw, so fetch the stream again. + // stream will be closed after the exception was thrown, so fetch the stream again. using (var scoreStream = archive.GetStream(name)) { scoreStream.CopyTo(stream); From 5fcd7363320485cf35d25715edaf057cf646f6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 13:52:45 +0200 Subject: [PATCH 20/35] Redo nano beatmap card design to fit needs better Wanting to use this inside notification, it turns out that the original design did not work very well at such narrow widths, and additionally the typical button setup borrowed from elsewhere resulted in teeny tiny action buttons. To that end, slim down the design (get rid of thumbnail, audio preview, make expandable right side slimmer), as well as change the entire panel so that it has only one action associated with it at all times, and clicking the panel in any place triggers that action. --- .../Drawables/Cards/BeatmapCardNano.cs | 37 +-- .../Cards/CollapsibleButtonContainerSlim.cs | 246 ++++++++++++++++++ 2 files changed, 267 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs index ba2142d28f..2f46bc51d6 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -20,15 +20,25 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Drawable IdleContent => idleBottomContent; protected override Drawable DownloadInProgressContent => downloadProgressBar; + public override float Width + { + get => base.Width; + set + { + base.Width = value; + + if (LoadState >= LoadState.Ready) + buttonContainer.Width = value; + } + } + private const float height = 60; private const float width = 300; - private const float cover_width = 80; [Cached] private readonly BeatmapCardContent content; - private BeatmapCardThumbnail thumbnail = null!; - private CollapsibleButtonContainer buttonContainer = null!; + private CollapsibleButtonContainerSlim buttonContainer = null!; private FillFlowContainer idleBottomContent = null!; private BeatmapCardDownloadProgressBar downloadProgressBar = null!; @@ -52,22 +62,16 @@ namespace osu.Game.Beatmaps.Drawables.Cards { c.MainContent = new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + Height = height, Children = new Drawable[] { - thumbnail = new BeatmapCardThumbnail(BeatmapSet) + buttonContainer = new CollapsibleButtonContainerSlim(BeatmapSet) { - Name = @"Left (icon) area", - Size = new Vector2(cover_width, height), - Padding = new MarginPadding { Right = CORNER_RADIUS }, - }, - buttonContainer = new CollapsibleButtonContainer(BeatmapSet) - { - X = cover_width - CORNER_RADIUS, - Width = width - cover_width + CORNER_RADIUS, + Width = Width, FavouriteState = { BindTarget = FavouriteState }, - ButtonsCollapsedWidth = CORNER_RADIUS, - ButtonsExpandedWidth = 30, + ButtonsCollapsedWidth = 5, + ButtonsExpandedWidth = 20, Children = new Drawable[] { new FillFlowContainer @@ -145,6 +149,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards }; c.Expanded.BindTarget = Expanded; }); + + Action = () => buttonContainer.TriggerClick(); } private LocalisableString createArtistText() @@ -160,7 +166,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards bool showDetails = IsHovered; buttonContainer.ShowDetails.Value = showDetails; - thumbnail.Dimmed.Value = showDetails; } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs new file mode 100644 index 0000000000..d17ff0d759 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs @@ -0,0 +1,246 @@ +// 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.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public partial class CollapsibleButtonContainerSlim : OsuClickableContainer + { + public Bindable ShowDetails = new Bindable(); + public Bindable FavouriteState = new Bindable(); + + private readonly BeatmapDownloadTracker downloadTracker; + + private float buttonsExpandedWidth; + + public float ButtonsExpandedWidth + { + get => buttonsExpandedWidth; + set + { + buttonsExpandedWidth = value; + buttonArea.Width = value; + if (IsLoaded) + updateState(); + } + } + + private float buttonsCollapsedWidth; + + public float ButtonsCollapsedWidth + { + get => buttonsCollapsedWidth; + set + { + buttonsCollapsedWidth = value; + if (IsLoaded) + updateState(); + } + } + + protected override Container Content => mainContent; + + private readonly APIBeatmapSet beatmapSet; + + private readonly Container background; + + private readonly Container buttonArea; + + private readonly Container mainArea; + private readonly Container mainContent; + + private readonly Container icons; + private readonly SpriteIcon downloadIcon; + private readonly LoadingSpinner spinner; + private readonly SpriteIcon goToBeatmapIcon; + + private const int icon_size = 12; + + private Bindable preferNoVideo = null!; + + [Resolved] + private BeatmapModelDownloader beatmaps { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public CollapsibleButtonContainerSlim(APIBeatmapSet beatmapSet) + { + this.beatmapSet = beatmapSet; + + downloadTracker = new BeatmapDownloadTracker(beatmapSet); + + RelativeSizeAxes = Axes.Y; + Masking = true; + CornerRadius = BeatmapCard.CORNER_RADIUS; + + base.Content.AddRange(new Drawable[] + { + downloadTracker, + background = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White + }, + }, + buttonArea = new Container + { + Name = @"Right (button) area", + RelativeSizeAxes = Axes.Y, + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + Child = icons = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + downloadIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size), + Icon = FontAwesome.Solid.Download + }, + spinner = new LoadingSpinner + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size) + }, + goToBeatmapIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size), + Icon = FontAwesome.Solid.AngleDoubleRight + }, + } + } + }, + mainArea = new Container + { + Name = @"Main content", + RelativeSizeAxes = Axes.Y, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Masking = true, + Children = new Drawable[] + { + new BeatmapCardContentBackground(beatmapSet) + { + RelativeSizeAxes = Axes.Both, + Dimmed = { BindTarget = ShowDetails } + }, + mainContent = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + } + } + } + }); + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); + + downloadIcon.Colour = spinner.Colour = colourProvider.Content1; + goToBeatmapIcon.Colour = colourProvider.Foreground1; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + preferNoVideo.BindValueChanged(_ => updateState()); + downloadTracker.State.BindValueChanged(_ => updateState()); + ShowDetails.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + private void updateState() + { + float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth); + + mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + var backgroundColour = downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3; + if (ShowDetails.Value) + backgroundColour = backgroundColour.Lighten(0.2f); + + background.FadeColour(backgroundColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + icons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + if (beatmapSet.Availability.DownloadDisabled) + { + Enabled.Value = false; + TooltipText = BeatmapsetsStrings.AvailabilityDisabled; + return; + } + + switch (downloadTracker.State.Value) + { + case DownloadState.NotDownloaded: + Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); + break; + + case DownloadState.LocallyAvailable: + Action = () => game?.PresentBeatmap(beatmapSet); + break; + + default: + Action = null; + break; + } + + downloadIcon.FadeTo(downloadTracker.State.Value == DownloadState.NotDownloaded ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + spinner.FadeTo(downloadTracker.State.Value == DownloadState.Downloading || downloadTracker.State.Value == DownloadState.Importing ? 1 : 0, + BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + goToBeatmapIcon.FadeTo(downloadTracker.State.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + if (downloadTracker.State.Value == DownloadState.NotDownloaded) + { + if (!beatmapSet.HasVideo) + TooltipText = BeatmapsetsStrings.PanelDownloadAll; + else + TooltipText = preferNoVideo.Value ? BeatmapsetsStrings.PanelDownloadNoVideo : BeatmapsetsStrings.PanelDownloadVideo; + } + else + { + TooltipText = default; + } + } + } +} From 47764b301290bb64a3fcf6c35466861cb3778532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 13:54:57 +0200 Subject: [PATCH 21/35] Fix `OsuTestScene.CreateAPIBeatmapSet()` not backlinking set beatmaps to the set --- osu.Game/Tests/Visual/OsuTestScene.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 0ec5a4c5c2..3ccb795a57 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -265,7 +265,7 @@ namespace osu.Game.Tests.Visual { Debug.Assert(original.BeatmapSet != null); - return new APIBeatmapSet + var result = new APIBeatmapSet { OnlineID = original.BeatmapSet.OnlineID, Status = BeatmapOnlineStatus.Ranked, @@ -301,6 +301,11 @@ namespace osu.Game.Tests.Visual } } }; + + foreach (var beatmap in result.Beatmaps) + beatmap.BeatmapSet = result; + + return result; } protected WorkingBeatmap CreateWorkingBeatmap(RulesetInfo ruleset) => From 775f96f06187edb24bc4dbbfa7577c4545ddacc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 13:55:03 +0200 Subject: [PATCH 22/35] Add very basic visual tests for missing beatmap notification The full-stack test using the whole 9 `OsuGameTest` yards is unusable for rapid development. I don't really get how you could ever design anything using it without tossing your computer out the window. --- .../TestSceneMissingBeatmapNotification.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs new file mode 100644 index 0000000000..23b9c5f76a --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Database; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public partial class TestSceneMissingBeatmapNotification : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + [BackgroundDependencyLoader] + private void load() + { + Child = new Container + { + Width = 280, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = new MissingBeatmapNotification(CreateAPIBeatmapSet(Ruleset.Value).Beatmaps.First(), new MemoryStream(), "deadbeef") + }; + } + } +} From 2709c6cd6773d1e770589c22d1f258faaa8d9895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 14:03:55 +0200 Subject: [PATCH 23/35] Integrate nano beatmap card into the notification --- .../Database/MissingBeatmapNotification.cs | 86 +++---------------- .../Overlays/Notifications/Notification.cs | 6 +- 2 files changed, 18 insertions(+), 74 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index d6674b9434..329d362bc4 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -6,19 +6,13 @@ using System.IO; 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; -using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Scoring; -using osuTK.Graphics; using Realms; namespace osu.Game.Database @@ -40,6 +34,7 @@ namespace osu.Game.Database private Bindable autodownloadConfig = null!; private Bindable noVideoSetting = null!; + private BeatmapCardNano card = null!; private IDisposable? realmSubscription; @@ -52,7 +47,7 @@ namespace osu.Game.Database } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, BeatmapSetOverlay? beatmapSetOverlay) + private void load(OsuConfigManager config) { Text = "You do not have the required beatmap for this replay"; @@ -70,69 +65,7 @@ namespace osu.Game.Database autodownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating); noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); - Content.Add(new ClickableContainer - { - RelativeSizeAxes = Axes.X, - Height = 70, - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopLeft, - CornerRadius = 4, - Masking = true, - Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSetInfo.OnlineID), - Children = new Drawable[] - { - new DelayedLoadWrapper(() => new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card) - { - OnlineInfo = beatmapSetInfo, - RelativeSizeAxes = Axes.Both - }) - { - RelativeSizeAxes = Axes.Both - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.4f - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding - { - Left = 10f, - Top = 5f - }, - Children = new Drawable[] - { - new TruncatingSpriteText - { - Text = beatmapSetInfo.Title, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - RelativeSizeAxes = Axes.X, - }, - new TruncatingSpriteText - { - Text = beatmapSetInfo.Artist, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12, italics: true), - RelativeSizeAxes = Axes.X, - } - } - }, - new BeatmapDownloadButton(beatmapSetInfo) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Width = 50, - Height = 30, - Margin = new MarginPadding - { - Bottom = 1f - } - } - } - }); + Content.Add(card = new BeatmapCardNano(beatmapSetInfo)); } protected override void LoadComplete() @@ -143,6 +76,15 @@ namespace osu.Game.Database beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value); } + protected override void Update() + { + base.Update(); + card.Width = Content.DrawWidth; + } + + protected override bool OnHover(HoverEvent e) => false; + protected override void OnHoverLost(HoverLostEvent e) { } + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) { if (changes?.InsertedIndices == null) return; diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 8cdc373417..805604c274 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Notifications public bool WasClosed { get; private set; } - private readonly Container content; + private readonly FillFlowContainer content; protected override Container Content => content; @@ -166,11 +166,13 @@ namespace osu.Game.Overlays.Notifications Padding = new MarginPadding(10), Children = new Drawable[] { - content = new Container + content = new FillFlowContainer { Masking = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(15) }, } }, From b2c98da3307a57be00ba7cb7ea710735f29a1067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 14:09:06 +0200 Subject: [PATCH 24/35] Reword and localise copy --- osu.Game/Database/MissingBeatmapNotification.cs | 5 +++-- osu.Game/Localisation/NotificationsStrings.cs | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 329d362bc4..e2c72f35de 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -14,6 +14,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Scoring; using Realms; +using osu.Game.Localisation; namespace osu.Game.Database { @@ -49,7 +50,7 @@ namespace osu.Game.Database [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - Text = "You do not have the required beatmap for this replay"; + Text = NotificationsStrings.MissingBeatmapForReplay; realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); @@ -58,7 +59,7 @@ namespace osu.Game.Database { if (r.All().Any(s => !s.DeletePending && s.OnlineID == beatmapSetInfo.OnlineID)) { - Text = "You have the corresponding beatmapset but no beatmap, you may need to update the beatmapset."; + Text = NotificationsStrings.MismatchingBeatmapForReplay; } }); diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 53687f2b28..194a1fa399 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -93,6 +93,16 @@ Please try changing your audio device to a working setting."); /// public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username); + /// + /// "You do not have the beatmap for this replay." + /// + public static LocalisableString MissingBeatmapForReplay => new TranslatableString(getKey(@"missing_beatmap_for_replay"), @"You do not have the beatmap for this replay."); + + /// + /// "Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it." + /// + public static LocalisableString MismatchingBeatmapForReplay => new TranslatableString(getKey(@"mismatching_beatmap_for_replay"), @"Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } From 27471ad170bfaf4e2df5e42520f9c19613bf67eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 14:30:06 +0200 Subject: [PATCH 25/35] Make missing beatmap notification simple Progress didn't work for several reasons: - It was spinning when nothing was actually happening yet (especially egregious with autodownload off) - It was blocking exits (as all progress notifications do) - When actually going to download, two progress notifications would pertain to one thing - It wasn't helping much with the actual implementation of score re-import, cancelling the progress notification would result in similarly jank UX of beatmap importing but not the score. --- osu.Game/Database/MissingBeatmapNotification.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index e2c72f35de..07382a0e3b 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -18,7 +18,7 @@ using osu.Game.Localisation; namespace osu.Game.Database { - public partial class MissingBeatmapNotification : ProgressNotification + public partial class MissingBeatmapNotification : SimpleNotification { [Resolved] private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; @@ -93,7 +93,7 @@ namespace osu.Game.Database if (sender.Any(s => s.Beatmaps.Any(b => b.MD5Hash == beatmapHash))) { var importTask = new ImportTask(scoreStream, "score.osr"); - scoreManager.Import(this, new[] { importTask }); + scoreManager.Import(new[] { importTask }); realmSubscription?.Dispose(); } } From 3bddf4bf9a1ba6f5de548d565ec260acd6035b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 14:46:24 +0200 Subject: [PATCH 26/35] Rename spectator-specific settings to more generic (with backwards migration) --- osu.Game/Configuration/OsuConfigManager.cs | 14 ++++++++++++-- .../Database/MissingBeatmapNotification.cs | 6 +++--- .../Localisation/OnlineSettingsStrings.cs | 5 +++++ osu.Game/Localisation/WebSettingsStrings.cs | 19 +++++++++++++++++++ .../Settings/Sections/Online/WebSettings.cs | 6 +++--- osu.Game/Screens/Play/SoloSpectator.cs | 2 +- 6 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Localisation/WebSettingsStrings.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 921284ad4d..b5253d3500 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -64,7 +64,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Token, string.Empty); - SetDefault(OsuSetting.AutomaticallyDownloadWhenSpectating, false); + SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false); SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled => { @@ -215,6 +215,12 @@ namespace osu.Game.Configuration // migrations can be added here using a condition like: // if (combined < 20220103) { performMigration() } + if (combined < 20230918) + { +#pragma warning disable CS0618 // Type or member is obsolete + SetValue(OsuSetting.AutomaticallyDownloadMissingBeatmaps, Get(OsuSetting.AutomaticallyDownloadWhenSpectating)); // can be removed 20240618 +#pragma warning restore CS0618 // Type or member is obsolete + } } public override TrackedSettings CreateTrackedSettings() @@ -383,13 +389,17 @@ namespace osu.Game.Configuration EditorShowHitMarkers, EditorAutoSeekOnPlacement, DiscordRichPresence, + + [Obsolete($"Use {nameof(AutomaticallyDownloadMissingBeatmaps)} instead.")] // can be removed 20240318 AutomaticallyDownloadWhenSpectating, + ShowOnlineExplicitContent, LastProcessedMetadataId, SafeAreaConsiderations, ComboColourNormalisationAmount, ProfileCoverExpanded, EditorLimitedDistanceSnap, - ReplaySettingsOverlay + ReplaySettingsOverlay, + AutomaticallyDownloadMissingBeatmaps, } } diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 07382a0e3b..9ae6ac3b58 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -33,7 +33,7 @@ namespace osu.Game.Database private readonly APIBeatmapSet beatmapSetInfo; private readonly string beatmapHash; - private Bindable autodownloadConfig = null!; + private Bindable autoDownloadConfig = null!; private Bindable noVideoSetting = null!; private BeatmapCardNano card = null!; @@ -63,7 +63,7 @@ namespace osu.Game.Database } }); - autodownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating); + autoDownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps); noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); Content.Add(card = new BeatmapCardNano(beatmapSetInfo)); @@ -73,7 +73,7 @@ namespace osu.Game.Database { base.LoadComplete(); - if (autodownloadConfig.Value) + if (autoDownloadConfig.Value) beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value); } diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs index 3200b1c75c..5ea53a13bf 100644 --- a/osu.Game/Localisation/OnlineSettingsStrings.cs +++ b/osu.Game/Localisation/OnlineSettingsStrings.cs @@ -59,6 +59,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AutomaticallyDownloadWhenSpectating => new TranslatableString(getKey(@"automatically_download_when_spectating"), @"Automatically download beatmaps when spectating"); + /// + /// "Automatically download missing beatmaps" + /// + public static LocalisableString AutomaticallyDownloadMissingBeatmaps => new TranslatableString(getKey(@"automatically_download_missing_beatmaps"), @"Automatically download missing beatmaps"); + /// /// "Show explicit content in search results" /// diff --git a/osu.Game/Localisation/WebSettingsStrings.cs b/osu.Game/Localisation/WebSettingsStrings.cs new file mode 100644 index 0000000000..b3033524dc --- /dev/null +++ b/osu.Game/Localisation/WebSettingsStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class WebSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.WebSettings"; + + /// + /// "Automatically download missing beatmaps" + /// + public static LocalisableString AutomaticallyDownloadMissingBeatmaps => new TranslatableString(getKey(@"automatically_download_missing_beatmaps"), @"Automatically download missing beatmaps"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index d34b01ebf3..ce5c85bed0 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -31,9 +31,9 @@ namespace osu.Game.Overlays.Settings.Sections.Online }, new SettingsCheckbox { - LabelText = OnlineSettingsStrings.AutomaticallyDownloadWhenSpectating, - Keywords = new[] { "spectator" }, - Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), + LabelText = OnlineSettingsStrings.AutomaticallyDownloadMissingBeatmaps, + Keywords = new[] { "spectator", "replay" }, + Current = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps), }, new SettingsCheckbox { diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index a5c84e97ab..f5af2684d3 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Play automaticDownload = new SettingsCheckbox { LabelText = "Automatically download beatmaps", - Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), + Current = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps), Anchor = Anchor.Centre, Origin = Anchor.Centre, }, From 4cdd19bb5a90c709baf08f322eabcd862ecb08e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 14:54:25 +0200 Subject: [PATCH 27/35] Use different copy when auto-downloading --- osu.Game/Database/MissingBeatmapNotification.cs | 4 ++-- osu.Game/Localisation/NotificationsStrings.cs | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 9ae6ac3b58..d9054980b1 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -50,8 +50,6 @@ namespace osu.Game.Database [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - Text = NotificationsStrings.MissingBeatmapForReplay; - realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); @@ -75,6 +73,8 @@ namespace osu.Game.Database if (autoDownloadConfig.Value) beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value); + + Text = autoDownloadConfig.Value ? NotificationsStrings.DownloadingBeatmapForReplay : NotificationsStrings.MissingBeatmapForReplay; } protected override void Update() diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 194a1fa399..adbd7a354b 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -98,6 +98,11 @@ Please try changing your audio device to a working setting."); /// public static LocalisableString MissingBeatmapForReplay => new TranslatableString(getKey(@"missing_beatmap_for_replay"), @"You do not have the beatmap for this replay."); + /// + /// "Downloading missing beatmap for this replay..." + /// + public static LocalisableString DownloadingBeatmapForReplay => new TranslatableString(getKey(@"downloading_beatmap_for_replay"), @"Downloading missing beatmap for this replay..."); + /// /// "Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it." /// From 25e43bd7d718489c81c35ae115b72a5a50a3de85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 14:54:36 +0200 Subject: [PATCH 28/35] Auto-close notification after successful download --- osu.Game/Database/MissingBeatmapNotification.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index d9054980b1..d98c07ce1f 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -95,6 +95,7 @@ namespace osu.Game.Database var importTask = new ImportTask(scoreStream, "score.osr"); scoreManager.Import(new[] { importTask }); realmSubscription?.Dispose(); + Close(false); } } From 8e199de78ac57a7d9ed959378eb17df40c7354e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 08:30:17 +0200 Subject: [PATCH 29/35] Tweak nano beatmap card UX further to meet expectations --- .../Drawables/Cards/BeatmapCardNano.cs | 2 - .../Cards/CollapsibleButtonContainerSlim.cs | 237 +++++++++++------- 2 files changed, 142 insertions(+), 97 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs index 2f46bc51d6..29f9d7ed2c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -149,8 +149,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards }; c.Expanded.BindTarget = Expanded; }); - - Action = () => buttonContainer.TriggerClick(); } private LocalisableString createArtistText() diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs index d17ff0d759..151c91f4c1 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -17,10 +18,11 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osuTK; +using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables.Cards { - public partial class CollapsibleButtonContainerSlim : OsuClickableContainer + public partial class CollapsibleButtonContainerSlim : Container { public Bindable ShowDetails = new Bindable(); public Bindable FavouriteState = new Bindable(); @@ -56,30 +58,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Container Content => mainContent; - private readonly APIBeatmapSet beatmapSet; - private readonly Container background; - private readonly Container buttonArea; + private readonly OsuClickableContainer buttonArea; private readonly Container mainArea; private readonly Container mainContent; - private readonly Container icons; - private readonly SpriteIcon downloadIcon; - private readonly LoadingSpinner spinner; - private readonly SpriteIcon goToBeatmapIcon; - private const int icon_size = 12; - private Bindable preferNoVideo = null!; - - [Resolved] - private BeatmapModelDownloader beatmaps { get; set; } = null!; - - [Resolved] - private OsuGame? game { get; set; } - [Resolved] private OsuColour colours { get; set; } = null!; @@ -88,15 +75,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards public CollapsibleButtonContainerSlim(APIBeatmapSet beatmapSet) { - this.beatmapSet = beatmapSet; - downloadTracker = new BeatmapDownloadTracker(beatmapSet); RelativeSizeAxes = Axes.Y; Masking = true; CornerRadius = BeatmapCard.CORNER_RADIUS; - base.Content.AddRange(new Drawable[] + InternalChildren = new Drawable[] { downloadTracker, background = new Container @@ -110,39 +95,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards Colour = Colour4.White }, }, - buttonArea = new Container + buttonArea = new ButtonArea(beatmapSet) { Name = @"Right (button) area", - RelativeSizeAxes = Axes.Y, - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Child = icons = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - downloadIcon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size), - Icon = FontAwesome.Solid.Download - }, - spinner = new LoadingSpinner - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size) - }, - goToBeatmapIcon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size), - Icon = FontAwesome.Solid.AngleDoubleRight - }, - } - } + State = { BindTarget = downloadTracker.State } }, mainArea = new Container { @@ -168,23 +124,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards } } } - }); - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); - - downloadIcon.Colour = spinner.Colour = colourProvider.Content1; - goToBeatmapIcon.Colour = colourProvider.Foreground1; + }; } protected override void LoadComplete() { base.LoadComplete(); - preferNoVideo.BindValueChanged(_ => updateState()); downloadTracker.State.BindValueChanged(_ => updateState()); ShowDetails.BindValueChanged(_ => updateState(), true); FinishTransforms(true); @@ -195,51 +141,152 @@ namespace osu.Game.Beatmaps.Drawables.Cards float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth); mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + buttonArea.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + } - var backgroundColour = downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3; - if (ShowDetails.Value) - backgroundColour = backgroundColour.Lighten(0.2f); + private partial class ButtonArea : OsuClickableContainer + { + public Bindable State { get; } = new Bindable(); - background.FadeColour(backgroundColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - icons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + private readonly APIBeatmapSet beatmapSet; - if (beatmapSet.Availability.DownloadDisabled) + private Box hoverLayer = null!; + private SpriteIcon downloadIcon = null!; + private LoadingSpinner spinner = null!; + private SpriteIcon goToBeatmapIcon = null!; + + private Bindable preferNoVideo = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private BeatmapModelDownloader beatmaps { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } + + public ButtonArea(APIBeatmapSet beatmapSet) { - Enabled.Value = false; - TooltipText = BeatmapsetsStrings.AvailabilityDisabled; - return; + this.beatmapSet = beatmapSet; } - switch (downloadTracker.State.Value) + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) { - case DownloadState.NotDownloaded: - Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); - break; + RelativeSizeAxes = Axes.Y; + Origin = Anchor.TopRight; + Anchor = Anchor.TopRight; + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = -BeatmapCard.CORNER_RADIUS }, + Child = hoverLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White.Opacity(0.1f), + Blending = BlendingParameters.Additive + } + }, + downloadIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size), + Icon = FontAwesome.Solid.Download + }, + spinner = new LoadingSpinner + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size) + }, + goToBeatmapIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size), + Icon = FontAwesome.Solid.AngleDoubleRight + }, + } + }; - case DownloadState.LocallyAvailable: - Action = () => game?.PresentBeatmap(beatmapSet); - break; - - default: - Action = null; - break; + preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); } - downloadIcon.FadeTo(downloadTracker.State.Value == DownloadState.NotDownloaded ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - spinner.FadeTo(downloadTracker.State.Value == DownloadState.Downloading || downloadTracker.State.Value == DownloadState.Importing ? 1 : 0, - BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - goToBeatmapIcon.FadeTo(downloadTracker.State.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - if (downloadTracker.State.Value == DownloadState.NotDownloaded) + protected override void LoadComplete() { - if (!beatmapSet.HasVideo) - TooltipText = BeatmapsetsStrings.PanelDownloadAll; + base.LoadComplete(); + + State.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + hoverLayer.FadeTo(IsHovered ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + downloadIcon.FadeTo(State.Value == DownloadState.NotDownloaded ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + downloadIcon.FadeColour(IsHovered ? colourProvider.Content1 : colourProvider.Light1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + spinner.FadeTo(State.Value == DownloadState.Downloading || State.Value == DownloadState.Importing ? 1 : 0, + BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + spinner.FadeColour(IsHovered ? colourProvider.Content1 : colourProvider.Light1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + goToBeatmapIcon.FadeTo(State.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + goToBeatmapIcon.FadeColour(IsHovered ? colourProvider.Foreground1 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + switch (State.Value) + { + case DownloadState.NotDownloaded: + Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); + break; + + case DownloadState.LocallyAvailable: + Action = () => game?.PresentBeatmap(beatmapSet); + break; + + default: + Action = null; + break; + } + + if (beatmapSet.Availability.DownloadDisabled) + { + Enabled.Value = false; + TooltipText = BeatmapsetsStrings.AvailabilityDisabled; + return; + } + + if (State.Value == DownloadState.NotDownloaded) + { + if (!beatmapSet.HasVideo) + TooltipText = BeatmapsetsStrings.PanelDownloadAll; + else + TooltipText = preferNoVideo.Value ? BeatmapsetsStrings.PanelDownloadNoVideo : BeatmapsetsStrings.PanelDownloadVideo; + } else - TooltipText = preferNoVideo.Value ? BeatmapsetsStrings.PanelDownloadNoVideo : BeatmapsetsStrings.PanelDownloadVideo; - } - else - { - TooltipText = default; + { + TooltipText = default; + } } } } From 0555d22eb8cc5edd3eaf1ab20e63d474bc75194c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 16:35:22 +0900 Subject: [PATCH 30/35] Add comment mentioning why hover is disabled on the notification type --- osu.Game/Database/MissingBeatmapNotification.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index d98c07ce1f..f2f7315e8b 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -83,6 +83,7 @@ namespace osu.Game.Database card.Width = Content.DrawWidth; } + // Disable hover so we don't have silly colour conflicts with the nested beatmap card. protected override bool OnHover(HoverEvent e) => false; protected override void OnHoverLost(HoverLostEvent e) { } From 7f30354e61d6d9fa8bc931b894f5040ff6d25c4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 17:20:58 +0900 Subject: [PATCH 31/35] Adjust sizing slightly to remove need for `CollapsibleButtonContainerSlim` --- .../Drawables/Cards/BeatmapCardNano.cs | 6 +- .../Cards/CollapsibleButtonContainerSlim.cs | 293 ------------------ .../Database/MissingBeatmapNotification.cs | 4 - 3 files changed, 3 insertions(+), 300 deletions(-) delete mode 100644 osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs index 29f9d7ed2c..4ab2b0c973 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -38,7 +38,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Cached] private readonly BeatmapCardContent content; - private CollapsibleButtonContainerSlim buttonContainer = null!; + private CollapsibleButtonContainer buttonContainer = null!; private FillFlowContainer idleBottomContent = null!; private BeatmapCardDownloadProgressBar downloadProgressBar = null!; @@ -66,12 +66,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards Height = height, Children = new Drawable[] { - buttonContainer = new CollapsibleButtonContainerSlim(BeatmapSet) + buttonContainer = new CollapsibleButtonContainer(BeatmapSet) { Width = Width, FavouriteState = { BindTarget = FavouriteState }, ButtonsCollapsedWidth = 5, - ButtonsExpandedWidth = 20, + ButtonsExpandedWidth = 30, Children = new Drawable[] { new FillFlowContainer diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs deleted file mode 100644 index 151c91f4c1..0000000000 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainerSlim.cs +++ /dev/null @@ -1,293 +0,0 @@ -// 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.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Game.Configuration; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; -using osu.Game.Resources.Localisation.Web; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Beatmaps.Drawables.Cards -{ - public partial class CollapsibleButtonContainerSlim : Container - { - public Bindable ShowDetails = new Bindable(); - public Bindable FavouriteState = new Bindable(); - - private readonly BeatmapDownloadTracker downloadTracker; - - private float buttonsExpandedWidth; - - public float ButtonsExpandedWidth - { - get => buttonsExpandedWidth; - set - { - buttonsExpandedWidth = value; - buttonArea.Width = value; - if (IsLoaded) - updateState(); - } - } - - private float buttonsCollapsedWidth; - - public float ButtonsCollapsedWidth - { - get => buttonsCollapsedWidth; - set - { - buttonsCollapsedWidth = value; - if (IsLoaded) - updateState(); - } - } - - protected override Container Content => mainContent; - - private readonly Container background; - - private readonly OsuClickableContainer buttonArea; - - private readonly Container mainArea; - private readonly Container mainContent; - - private const int icon_size = 12; - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - public CollapsibleButtonContainerSlim(APIBeatmapSet beatmapSet) - { - downloadTracker = new BeatmapDownloadTracker(beatmapSet); - - RelativeSizeAxes = Axes.Y; - Masking = true; - CornerRadius = BeatmapCard.CORNER_RADIUS; - - InternalChildren = new Drawable[] - { - downloadTracker, - background = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White - }, - }, - buttonArea = new ButtonArea(beatmapSet) - { - Name = @"Right (button) area", - State = { BindTarget = downloadTracker.State } - }, - mainArea = new Container - { - Name = @"Main content", - RelativeSizeAxes = Axes.Y, - CornerRadius = BeatmapCard.CORNER_RADIUS, - Masking = true, - Children = new Drawable[] - { - new BeatmapCardContentBackground(beatmapSet) - { - RelativeSizeAxes = Axes.Both, - Dimmed = { BindTarget = ShowDetails } - }, - mainContent = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Horizontal = 10, - Vertical = 4 - }, - } - } - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - downloadTracker.State.BindValueChanged(_ => updateState()); - ShowDetails.BindValueChanged(_ => updateState(), true); - FinishTransforms(true); - } - - private void updateState() - { - float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth); - - mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - buttonArea.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - } - - private partial class ButtonArea : OsuClickableContainer - { - public Bindable State { get; } = new Bindable(); - - private readonly APIBeatmapSet beatmapSet; - - private Box hoverLayer = null!; - private SpriteIcon downloadIcon = null!; - private LoadingSpinner spinner = null!; - private SpriteIcon goToBeatmapIcon = null!; - - private Bindable preferNoVideo = null!; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - [Resolved] - private BeatmapModelDownloader beatmaps { get; set; } = null!; - - [Resolved] - private OsuGame? game { get; set; } - - public ButtonArea(APIBeatmapSet beatmapSet) - { - this.beatmapSet = beatmapSet; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - RelativeSizeAxes = Axes.Y; - Origin = Anchor.TopRight; - Anchor = Anchor.TopRight; - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = -BeatmapCard.CORNER_RADIUS }, - Child = hoverLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White.Opacity(0.1f), - Blending = BlendingParameters.Additive - } - }, - downloadIcon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size), - Icon = FontAwesome.Solid.Download - }, - spinner = new LoadingSpinner - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size) - }, - goToBeatmapIcon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(icon_size), - Icon = FontAwesome.Solid.AngleDoubleRight - }, - } - }; - - preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - State.BindValueChanged(_ => updateState(), true); - FinishTransforms(true); - } - - protected override bool OnHover(HoverEvent e) - { - updateState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - private void updateState() - { - hoverLayer.FadeTo(IsHovered ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - downloadIcon.FadeTo(State.Value == DownloadState.NotDownloaded ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - downloadIcon.FadeColour(IsHovered ? colourProvider.Content1 : colourProvider.Light1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - spinner.FadeTo(State.Value == DownloadState.Downloading || State.Value == DownloadState.Importing ? 1 : 0, - BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - spinner.FadeColour(IsHovered ? colourProvider.Content1 : colourProvider.Light1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - goToBeatmapIcon.FadeTo(State.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - goToBeatmapIcon.FadeColour(IsHovered ? colourProvider.Foreground1 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - switch (State.Value) - { - case DownloadState.NotDownloaded: - Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); - break; - - case DownloadState.LocallyAvailable: - Action = () => game?.PresentBeatmap(beatmapSet); - break; - - default: - Action = null; - break; - } - - if (beatmapSet.Availability.DownloadDisabled) - { - Enabled.Value = false; - TooltipText = BeatmapsetsStrings.AvailabilityDisabled; - return; - } - - if (State.Value == DownloadState.NotDownloaded) - { - if (!beatmapSet.HasVideo) - TooltipText = BeatmapsetsStrings.PanelDownloadAll; - else - TooltipText = preferNoVideo.Value ? BeatmapsetsStrings.PanelDownloadNoVideo : BeatmapsetsStrings.PanelDownloadVideo; - } - else - { - TooltipText = default; - } - } - } - } -} diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index f2f7315e8b..bc96625ead 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -83,10 +83,6 @@ namespace osu.Game.Database card.Width = Content.DrawWidth; } - // Disable hover so we don't have silly colour conflicts with the nested beatmap card. - protected override bool OnHover(HoverEvent e) => false; - protected override void OnHoverLost(HoverLostEvent e) { } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) { if (changes?.InsertedIndices == null) return; From 0593c76c57436757e7da3ada6556c683396fdbfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 17:34:24 +0900 Subject: [PATCH 32/35] Fix log output using incorrect name --- osu.Game/Scoring/ScoreImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 2875035e1b..26594fb815 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -56,7 +56,7 @@ namespace osu.Game.Scoring catch (LegacyScoreDecoder.BeatmapNotFoundException e) { onMissingBeatmap(e, archive, name); - Logger.Log($@"Score '{name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database); + Logger.Log($@"Score '{archive.Name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database); return null; } } From f726c38215b6ff35305969b7c7e79f89a25818f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 17:41:00 +0900 Subject: [PATCH 33/35] Pass `ArchiveReader` instead of `Stream` to simplify resolution code --- .../TestSceneMissingBeatmapNotification.cs | 4 +-- .../Database/MissingBeatmapNotification.cs | 14 ++++----- osu.Game/Scoring/ScoreImporter.cs | 30 ++++--------------- 3 files changed, 14 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs index 23b9c5f76a..f5506edf3b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMissingBeatmapNotification.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -9,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Database; using osu.Game.Overlays; +using osu.Game.Tests.Scores.IO; namespace osu.Game.Tests.Visual.UserInterface { @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.UserInterface AutoSizeAxes = Axes.Y, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = new MissingBeatmapNotification(CreateAPIBeatmapSet(Ruleset.Value).Beatmaps.First(), new MemoryStream(), "deadbeef") + Child = new MissingBeatmapNotification(CreateAPIBeatmapSet(Ruleset.Value).Beatmaps.First(), new ImportScoreTest.TestArchiveReader(), "deadbeef") }; } } diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index bc96625ead..261de2a938 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -2,19 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.IO; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; +using osu.Game.IO.Archives; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Scoring; using Realms; -using osu.Game.Localisation; namespace osu.Game.Database { @@ -29,7 +28,7 @@ namespace osu.Game.Database [Resolved] private RealmAccess realm { get; set; } = null!; - private readonly MemoryStream scoreStream; + private readonly ArchiveReader scoreArchive; private readonly APIBeatmapSet beatmapSetInfo; private readonly string beatmapHash; @@ -39,12 +38,12 @@ namespace osu.Game.Database private IDisposable? realmSubscription; - public MissingBeatmapNotification(APIBeatmap beatmap, MemoryStream scoreStream, string beatmapHash) + public MissingBeatmapNotification(APIBeatmap beatmap, ArchiveReader scoreArchive, string beatmapHash) { beatmapSetInfo = beatmap.BeatmapSet!; this.beatmapHash = beatmapHash; - this.scoreStream = scoreStream; + this.scoreArchive = scoreArchive; } [BackgroundDependencyLoader] @@ -89,7 +88,8 @@ namespace osu.Game.Database if (sender.Any(s => s.Beatmaps.Any(b => b.MD5Hash == beatmapHash))) { - var importTask = new ImportTask(scoreStream, "score.osr"); + string name = scoreArchive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)); + var importTask = new ImportTask(scoreArchive.GetStream(name), name); scoreManager.Import(new[] { importTask }); realmSubscription?.Dispose(); Close(false); diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 26594fb815..b85b6a066e 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Threading; using Newtonsoft.Json; @@ -55,36 +54,17 @@ namespace osu.Game.Scoring } catch (LegacyScoreDecoder.BeatmapNotFoundException e) { - onMissingBeatmap(e, archive, name); Logger.Log($@"Score '{archive.Name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database); + + // In the case of a missing beatmap, let's attempt to resolve it and show a prompt to the user to download the required beatmap. + var req = new GetBeatmapRequest(new BeatmapInfo { MD5Hash = e.Hash }); + req.Success += res => PostNotification?.Invoke(new MissingBeatmapNotification(res, archive, e.Hash)); + api.Queue(req); return null; } } } - private void onMissingBeatmap(LegacyScoreDecoder.BeatmapNotFoundException e, ArchiveReader archive, string name) - { - var stream = new MemoryStream(); - - // stream will be closed after the exception was thrown, so fetch the stream again. - using (var scoreStream = archive.GetStream(name)) - { - scoreStream.CopyTo(stream); - } - - var req = new GetBeatmapRequest(new BeatmapInfo - { - MD5Hash = e.Hash - }); - - req.Success += res => - { - PostNotification?.Invoke(new MissingBeatmapNotification(res, stream, e.Hash)); - }; - - api.Queue(req); - } - public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); protected override void Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) From cdb5fea513f06bf18eefc62a8b0d4997d4c8a91d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 17:53:00 +0900 Subject: [PATCH 34/35] Remove unused translations --- .../Localisation/OnlineSettingsStrings.cs | 5 ----- osu.Game/Localisation/WebSettingsStrings.cs | 19 ------------------- 2 files changed, 24 deletions(-) delete mode 100644 osu.Game/Localisation/WebSettingsStrings.cs diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs index 5ea53a13bf..0660bac172 100644 --- a/osu.Game/Localisation/OnlineSettingsStrings.cs +++ b/osu.Game/Localisation/OnlineSettingsStrings.cs @@ -54,11 +54,6 @@ namespace osu.Game.Localisation /// public static LocalisableString PreferNoVideo => new TranslatableString(getKey(@"prefer_no_video"), @"Prefer downloads without video"); - /// - /// "Automatically download beatmaps when spectating" - /// - public static LocalisableString AutomaticallyDownloadWhenSpectating => new TranslatableString(getKey(@"automatically_download_when_spectating"), @"Automatically download beatmaps when spectating"); - /// /// "Automatically download missing beatmaps" /// diff --git a/osu.Game/Localisation/WebSettingsStrings.cs b/osu.Game/Localisation/WebSettingsStrings.cs deleted file mode 100644 index b3033524dc..0000000000 --- a/osu.Game/Localisation/WebSettingsStrings.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.Localisation; - -namespace osu.Game.Localisation -{ - public static class WebSettingsStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.WebSettings"; - - /// - /// "Automatically download missing beatmaps" - /// - public static LocalisableString AutomaticallyDownloadMissingBeatmaps => new TranslatableString(getKey(@"automatically_download_missing_beatmaps"), @"Automatically download missing beatmaps"); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} \ No newline at end of file From ed9039f60f31415089be387e9075e6439099ee58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 11:09:23 +0200 Subject: [PATCH 35/35] Fix notification text sets overwriting each other --- .../Database/MissingBeatmapNotification.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Database/MissingBeatmapNotification.cs b/osu.Game/Database/MissingBeatmapNotification.cs index 261de2a938..584b2675f3 100644 --- a/osu.Game/Database/MissingBeatmapNotification.cs +++ b/osu.Game/Database/MissingBeatmapNotification.cs @@ -52,14 +52,6 @@ namespace osu.Game.Database realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); - realm.Run(r => - { - if (r.All().Any(s => !s.DeletePending && s.OnlineID == beatmapSetInfo.OnlineID)) - { - Text = NotificationsStrings.MismatchingBeatmapForReplay; - } - }); - autoDownloadConfig = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps); noVideoSetting = config.GetBindable(OsuSetting.PreferNoVideo); @@ -71,9 +63,15 @@ namespace osu.Game.Database base.LoadComplete(); if (autoDownloadConfig.Value) + { + Text = NotificationsStrings.DownloadingBeatmapForReplay; beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value); - - Text = autoDownloadConfig.Value ? NotificationsStrings.DownloadingBeatmapForReplay : NotificationsStrings.MissingBeatmapForReplay; + } + else + { + bool missingSetMatchesExistingOnlineId = realm.Run(r => r.All().Any(s => !s.DeletePending && s.OnlineID == beatmapSetInfo.OnlineID)); + Text = missingSetMatchesExistingOnlineId ? NotificationsStrings.MismatchingBeatmapForReplay : NotificationsStrings.MissingBeatmapForReplay; + } } protected override void Update()