From eef63b41dae7bc1d7916236b0bb2f0f6800cca4c Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 2 Aug 2023 15:01:58 +0900 Subject: [PATCH 01/71] 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/71] 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/71] 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/71] 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/71] 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/71] 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/71] 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/71] 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/71] 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/71] 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/71] 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/71] 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 e321303ef665a623436a9bf0e04cf91aebd3323b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Aug 2023 12:32:42 +0900 Subject: [PATCH 13/71] Add application category type to enable game mode on new macOS versions --- osu.iOS/Info.plist | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 0ce1d952d0..cf51fe995b 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -34,9 +34,9 @@ CADisableMinimumFrameDurationOnPhone NSCameraUsageDescription - We don't really use the camera. + We don't really use the camera. NSMicrophoneUsageDescription - We don't really use the microphone. + We don't really use the microphone. UISupportedInterfaceOrientations UIInterfaceOrientationLandscapeRight @@ -130,5 +130,7 @@ Editor + LSApplicationCategoryType + public.app-category.music-games From 58844092d6984f96e5b7f1fd263d675979c8b879 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 4 Sep 2023 16:17:21 +0900 Subject: [PATCH 14/71] 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 15/71] 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 16/71] 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 17/71] 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 18/71] 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 19/71] 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 68752f95e516459ea1ab814812919b381d78c45d Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 6 Sep 2023 22:49:13 +0900 Subject: [PATCH 20/71] color friend score to pink --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 31 +++++++++++++++++++ .../Play/HUD/GameplayLeaderboardScore.cs | 15 ++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index d4000c07e7..418e0ad981 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -6,11 +6,14 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Play.HUD; using osuTK; @@ -139,6 +142,29 @@ namespace osu.Game.Tests.Visual.Gameplay => AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount); } + [Test] + public void TestFriendScore() + { + APIUser friend = new APIUser { Username = "my friend", Id = 10000 }; + + createLeaderboard(); + addLocalPlayer(); + + AddStep("initialize api", () => + { + var api = (DummyAPIAccess)API; + + api.Friends.Add(friend); + }); + + int playerNumber = 1; + AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3); + AddUntilStep("there are no pink color score", () => leaderboard.ChildrenOfType().All(b => b.Colour != Color4Extensions.FromHex("ff549a"))); + + AddRepeatStep("add 3 friend score", () => createRandomScore(friend), 3); + AddUntilStep("there are pink color for friend score", () => leaderboard.GetScoreByUsername("my friend").ChildrenOfType().Any(b => b.Colour == Color4Extensions.FromHex("ff549a"))); + } + private void addLocalPlayer() { AddStep("add local player", () => @@ -179,6 +205,11 @@ namespace osu.Game.Tests.Visual.Gameplay return scoreItem != null && scoreItem.ScorePosition == expectedPosition; } + + public GameplayLeaderboardScore GetScoreByUsername(string username) + { + return Flow.FirstOrDefault(i => i.User?.Username == username); + } } } } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index dcb2c1071e..502303e80c 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -11,6 +12,8 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Scoring; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -107,6 +110,8 @@ namespace osu.Game.Screens.Play.HUD private IBindable scoreDisplayMode = null!; + private readonly IBindableList apiFriends = new BindableList(); + /// /// Creates a new . /// @@ -124,7 +129,7 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager osuConfigManager) + private void load(OsuColour colours, OsuConfigManager osuConfigManager, IAPIProvider api) { Container avatarContainer; @@ -311,6 +316,9 @@ namespace osu.Game.Screens.Play.HUD }, true); HasQuit.BindValueChanged(_ => updateState()); + + apiFriends.BindTo(api.Friends); + apiFriends.BindCollectionChanged((_, _) => updateState()); } protected override void LoadComplete() @@ -389,6 +397,11 @@ namespace osu.Game.Screens.Play.HUD panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966"); textColour = TextColour ?? Color4Extensions.FromHex("2e576b"); } + else if (apiFriends.Any(f => User?.Equals(f) == true)) + { + panelColour = BackgroundColour ?? Color4Extensions.FromHex("ff549a"); + textColour = TextColour ?? Color4.White; + } else { panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc"); From 87ec33bb66bd35e15b67ce86ddced6d62adfe80b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Sep 2023 14:50:22 +0900 Subject: [PATCH 21/71] Tidy up test --- .../Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 418e0ad981..65f943d36b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -150,14 +150,16 @@ namespace osu.Game.Tests.Visual.Gameplay createLeaderboard(); addLocalPlayer(); - AddStep("initialize api", () => + AddStep("Add friend to API", () => { var api = (DummyAPIAccess)API; + api.Friends.Clear(); api.Friends.Add(friend); }); int playerNumber = 1; + AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3); AddUntilStep("there are no pink color score", () => leaderboard.ChildrenOfType().All(b => b.Colour != Color4Extensions.FromHex("ff549a"))); From 5b2af7f264cba42dbb4ad4dababb38a48eff2cf9 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Wed, 13 Sep 2023 12:44:00 +0200 Subject: [PATCH 22/71] Default to none bank if invalid samplebank is specified --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 3dbe7b6519..339e9bb5bc 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -193,10 +193,10 @@ namespace osu.Game.Rulesets.Objects.Legacy var addBank = (LegacySampleBank)Parsing.ParseInt(split[1]); string stringBank = bank.ToString().ToLowerInvariant(); - if (stringBank == @"none") + if (stringBank == @"none" || !Enum.IsDefined(bank)) stringBank = null; string stringAddBank = addBank.ToString().ToLowerInvariant(); - if (stringAddBank == @"none") + if (stringAddBank == @"none" || !Enum.IsDefined(addBank)) stringAddBank = null; bankInfo.BankForNormal = stringBank; From 8f9cde01aae3ea874a8225c40fedacd637ec0c2e Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Wed, 13 Sep 2023 13:38:13 +0200 Subject: [PATCH 23/71] Add test --- .../Formats/LegacyBeatmapDecoderTest.cs | 27 +++++++++++++++++++ osu.Game.Tests/Resources/invalid-bank.osu | 11 ++++++++ 2 files changed, 38 insertions(+) create mode 100644 osu.Game.Tests/Resources/invalid-bank.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 970b6aaf60..6fe9c902bb 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -621,6 +621,33 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestInvalidBankDefaultsToNone() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("invalid-bank.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var hitObjects = decoder.Decode(stream).HitObjects; + + Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[0].Samples[0].Bank); + Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[0].Samples[1].Bank); + + Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[1].Samples[0].Bank); + Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[1].Samples[1].Bank); + + Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[2].Samples[0].Bank); + Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[2].Samples[1].Bank); + + Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[3].Samples[0].Bank); + Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[3].Samples[1].Bank); + + Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[4].Samples[0].Bank); + Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[4].Samples[1].Bank); + } + } + [Test] public void TestFallbackDecoderForCorruptedHeader() { diff --git a/osu.Game.Tests/Resources/invalid-bank.osu b/osu.Game.Tests/Resources/invalid-bank.osu new file mode 100644 index 0000000000..fb54a61fd3 --- /dev/null +++ b/osu.Game.Tests/Resources/invalid-bank.osu @@ -0,0 +1,11 @@ +osu file format v14 + +[General] +SampleSet: Normal + +[HitObjects] +256,192,1000,1,8,0:0:0:0: +256,192,2000,1,8,1:2:0:0: +256,192,3000,1,8,2:62:0:0: +256,192,4000,1,8,41:2:0:0: +256,192,5000,1,8,41:62:0:0: From 900376b662ab31b975cda9d6cd4e21dca72e4932 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Sep 2023 16:06:59 +0900 Subject: [PATCH 24/71] Refactor storyboard resource lookup to be more streamlined --- .../Drawables/DrawableStoryboard.cs | 53 ++++++++++++++++--- .../Drawables/DrawableStoryboardAnimation.cs | 6 +-- .../Drawables/DrawableStoryboardSprite.cs | 2 +- osu.Game/Storyboards/Storyboard.cs | 13 ++--- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index e674e7512c..6931cea81e 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -1,22 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; -using osuTK; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; +using osuTK; namespace osu.Game.Storyboards.Drawables { @@ -57,12 +58,18 @@ namespace osu.Game.Storyboards.Drawables [Cached(typeof(IReadOnlyList))] public IReadOnlyList Mods { get; } - private DependencyContainer dependencies; + [Resolved] + private GameHost host { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private DependencyContainer dependencies = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - public DrawableStoryboard(Storyboard storyboard, IReadOnlyList mods = null) + public DrawableStoryboard(Storyboard storyboard, IReadOnlyList? mods = null) { Storyboard = storyboard; Mods = mods ?? Array.Empty(); @@ -85,12 +92,15 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(IGameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmAccess realm) + private void load(IGameplayClock? clock, CancellationToken? cancellationToken) { if (clock != null) Clock = clock; - dependencies.Cache(new TextureStore(host.Renderer, host.CreateTextureLoaderStore(new RealmFileStore(realm, host.Storage).Store), false, scaleAdjust: 1)); + dependencies.CacheAs(typeof(TextureStore), + new TextureStore(host.Renderer, host.CreateTextureLoaderStore( + CreateResourceLookupStore() + ), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) { @@ -102,6 +112,8 @@ namespace osu.Game.Storyboards.Drawables lastEventEndTime = Storyboard.LatestEventTime; } + protected virtual IResourceStore CreateResourceLookupStore() => new StoryboardResourceLookupStore(Storyboard, realm, host); + protected override void Update() { base.Update(); @@ -115,5 +127,32 @@ namespace osu.Game.Storyboards.Drawables foreach (var layer in Children) layer.Enabled = passing ? layer.Layer.VisibleWhenPassing : layer.Layer.VisibleWhenFailing; } + + private class StoryboardResourceLookupStore : IResourceStore + { + private readonly IResourceStore realmFileStore; + private readonly Storyboard storyboard; + + public StoryboardResourceLookupStore(Storyboard storyboard, RealmAccess realm, GameHost host) + { + realmFileStore = new RealmFileStore(realm, host.Storage).Store; + this.storyboard = storyboard; + } + + public void Dispose() => + realmFileStore.Dispose(); + + public byte[] Get(string name) => + realmFileStore.Get(storyboard.GetStoragePathFromStoryboardPath(name)); + + public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => + realmFileStore.GetAsync(storyboard.GetStoragePathFromStoryboardPath(name), cancellationToken); + + public Stream GetStream(string name) => + realmFileStore.GetStream(storyboard.GetStoragePathFromStoryboardPath(name)); + + public IEnumerable GetAvailableResources() => + realmFileStore.GetAvailableResources(); + } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 82c01ea6a1..054a50456b 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -99,15 +99,13 @@ namespace osu.Game.Storyboards.Drawables { int frameIndex = 0; - Texture frameTexture = storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore); + Texture frameTexture = textureStore.Get(getFramePath(frameIndex)); if (frameTexture != null) { // sourcing from storyboard. for (frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++) - { - AddFrame(storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore), Animation.FrameDelay); - } + AddFrame(textureStore.Get(getFramePath(frameIndex)), Animation.FrameDelay); } else if (storyboard.UseSkinSprites) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index ec0cb7ca19..379de1a497 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -90,7 +90,7 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader] private void load(TextureStore textureStore, Storyboard storyboard) { - Texture = storyboard.GetTextureFromPath(Sprite.Path, textureStore); + Texture = textureStore.Get(Sprite.Path); if (Texture == null && storyboard.UseSkinSprites) { diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 566e064aad..1892855d3d 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Storyboards.Drawables; @@ -92,7 +91,7 @@ namespace osu.Game.Storyboards private static readonly string[] image_extensions = { @".png", @".jpg" }; - public Texture? GetTextureFromPath(string path, TextureStore textureStore) + public virtual string? GetStoragePathFromStoryboardPath(string path) { string? resolvedPath = null; @@ -102,10 +101,7 @@ namespace osu.Game.Storyboards } else { - // Just doing this extension logic locally here for simplicity. - // - // A more "sane" path may be to use the ISkinSource.GetTexture path (which will use the extensions of the underlying TextureStore), - // but comes with potential complexity (what happens if the user has beatmap skins disabled?). + // Some old storyboards don't include a file extension, so let's best guess at one. foreach (string ext in image_extensions) { if ((resolvedPath = BeatmapInfo.BeatmapSet?.GetPathForFile($"{path}{ext}")) != null) @@ -113,10 +109,7 @@ namespace osu.Game.Storyboards } } - if (!string.IsNullOrEmpty(resolvedPath)) - return textureStore.Get(resolvedPath); - - return null; + return resolvedPath; } } } From 46126719ebb3d6c56fc479bb097eb9fecf5caaa2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Sep 2023 16:57:17 +0900 Subject: [PATCH 25/71] Fix slider tests not correctly passing slider velocity to slideres --- .../TestSceneSlider.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 4ad78a3190..6c78ac51b4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -19,7 +19,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; -using osu.Game.Beatmaps.Legacy; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; @@ -206,7 +205,7 @@ namespace osu.Game.Rulesets.Osu.Tests StackHeight = 10 }; - return createDrawable(slider, 2, 2); + return createDrawable(slider, 2); } private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats); @@ -229,6 +228,7 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { + SliderVelocityMultiplier = speedMultiplier, StartTime = Time.Current + time_offset, Position = new Vector2(0, -(distance / 2)), Path = new SliderPath(PathType.PerfectCurve, new[] @@ -240,7 +240,7 @@ namespace osu.Game.Rulesets.Osu.Tests StackHeight = stackHeight }; - return createDrawable(slider, circleSize, speedMultiplier); + return createDrawable(slider, circleSize); } private Drawable testPerfect(int repeats = 0) @@ -258,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.Tests RepeatCount = repeats, }; - return createDrawable(slider, 2, 3); + return createDrawable(slider, 2); } private Drawable testLinear(int repeats = 0) => createLinear(repeats); @@ -281,7 +281,7 @@ namespace osu.Game.Rulesets.Osu.Tests RepeatCount = repeats, }; - return createDrawable(slider, 2, 3); + return createDrawable(slider, 2); } private Drawable testBezier(int repeats = 0) => createBezier(repeats); @@ -303,7 +303,7 @@ namespace osu.Game.Rulesets.Osu.Tests RepeatCount = repeats, }; - return createDrawable(slider, 2, 3); + return createDrawable(slider, 2); } private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats); @@ -326,7 +326,7 @@ namespace osu.Game.Rulesets.Osu.Tests RepeatCount = repeats, }; - return createDrawable(slider, 2, 3); + return createDrawable(slider, 2); } private Drawable testCatmull(int repeats = 0) => createCatmull(repeats); @@ -352,15 +352,12 @@ namespace osu.Game.Rulesets.Osu.Tests NodeSamples = repeatSamples }; - return createDrawable(slider, 3, 1); + return createDrawable(slider, 3); } - private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier) + private Drawable createDrawable(Slider slider, float circleSize) { - var cpi = new LegacyControlPointInfo(); - cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier }); - - slider.ApplyDefaults(cpi, new BeatmapDifficulty + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 From e3e7a81ad9f88936ae6a58e7a3402e8716dc590d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Sep 2023 16:58:47 +0900 Subject: [PATCH 26/71] Remove slider head circle movement (and remove setting from "classic" mod) --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 4 --- .../Objects/Drawables/DrawableSliderHead.cs | 25 ------------------- .../Skinning/Default/PlaySliderBody.cs | 16 ------------ 3 files changed, 45 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 0148ec1987..8930b4ad70 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -25,9 +25,6 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true); - [SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")] - public Bindable NoSliderHeadMovement { get; } = new BindableBool(true); - [SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")] public Bindable ClassicNoteLock { get; } = new BindableBool(true); @@ -71,7 +68,6 @@ namespace osu.Game.Rulesets.Osu.Mods switch (obj) { case DrawableSliderHead head: - head.TrackFollowCircle = !NoSliderHeadMovement.Value; if (FadeHitCircleEarly.Value && !usingHiddenFading) applyEarlyFading(head); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 41f6a40c0a..2dea05da2f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -3,11 +3,9 @@ #nullable disable -using System; using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Bindables; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; @@ -24,12 +22,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult; - /// - /// Makes this track the follow circle when the start time is reached. - /// If false, this will be pinned to its initial position in the slider. - /// - public bool TrackFollowCircle = true; - private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; @@ -64,23 +56,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit; } - protected override void Update() - { - base.Update(); - - Debug.Assert(Slider != null); - Debug.Assert(HitObject != null); - - if (TrackFollowCircle) - { - double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1); - - //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. - if (!IsHit) - Position = Slider.CurvePositionAt(completionProgress); - } - } - protected override HitResult ResultFor(double timeOffset) { Debug.Assert(HitObject != null); diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index 539777dd6b..aa507cbaf0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -46,22 +46,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1; BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; - - drawableObject.HitObjectApplied += onHitObjectApplied; - } - - private void onHitObjectApplied(DrawableHitObject obj) - { - var drawableSlider = (DrawableSlider)obj; - if (drawableSlider.HitObject == null) - return; - - // When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way. - if (!drawableSlider.HeadCircle.TrackFollowCircle) - { - SnakingOut.UnbindFrom(configSnakingOut); - SnakingOut.Value = false; - } } protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) => From 1ef0c92962a54b7173e6d4211129015f0b9d3742 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Sep 2023 17:23:57 +0900 Subject: [PATCH 27/71] Inverse snaking toggle --- osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 6c78ac51b4..7a38e72bc9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -34,16 +34,16 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; - private readonly BindableBool snakingIn = new BindableBool(); - private readonly BindableBool snakingOut = new BindableBool(); + private readonly BindableBool snakingIn = new BindableBool(true); + private readonly BindableBool snakingOut = new BindableBool(true); [SetUpSteps] public void SetUpSteps() { - AddToggleStep("toggle snaking", v => + AddToggleStep("disable snaking", v => { - snakingIn.Value = v; - snakingOut.Value = v; + snakingIn.Value = !v; + snakingOut.Value = !v; }); } From ec824140900bb03a0502903e88f64e6144310907 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Sep 2023 17:49:22 +0900 Subject: [PATCH 28/71] Allow testing hitting sliders are certain points in tests --- .../TestSceneSlider.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 7a38e72bc9..e005d7cac3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Audio; @@ -37,14 +38,22 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly BindableBool snakingIn = new BindableBool(true); private readonly BindableBool snakingOut = new BindableBool(true); - [SetUpSteps] - public void SetUpSteps() + private float progressToHit; + + protected override void LoadComplete() { + base.LoadComplete(); + AddToggleStep("disable snaking", v => { snakingIn.Value = !v; snakingOut.Value = !v; }); + + AddSliderStep("hit at", 0f, 1f, 0f, v => + { + progressToHit = v; + }); } [BackgroundDependencyLoader] @@ -55,6 +64,18 @@ namespace osu.Game.Rulesets.Osu.Tests config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); } + protected override void Update() + { + base.Update(); + + foreach (var slider in this.ChildrenOfType()) + { + double completionProgress = Math.Clamp((Time.Current - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1); + if (completionProgress > progressToHit && !slider.IsHit) + slider.HeadCircle.HitArea.Hit(); + } + } + [Test] public void TestVariousSliders() { From cf9ca60b0902ae267aac8d50d46e934deb5759ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Sep 2023 17:49:38 +0900 Subject: [PATCH 29/71] Change slider body to not animate snaking until head circle is (successfully) hit --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 01174d4d61..09d98654c3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1); Ball.UpdateProgress(completionProgress); - SliderBody?.UpdateProgress(completionProgress); + SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0); foreach (DrawableHitObject hitObject in NestedHitObjects) { From 45751dd1f2cf5610daf4d9cb966347fb453ef570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 12 Sep 2023 14:05:58 +0200 Subject: [PATCH 30/71] Minimum viable changes for ruleset-specific scoring test scenes --- .../TestSceneScoring.cs | 33 ++ .../Tests/Visual/Gameplay/ScoringTestScene.cs | 556 +++++++++--------- 2 files changed, 311 insertions(+), 278 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs rename osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs => osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs (53%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..5dbfc8d3a1 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual.Gameplay; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public partial class TestSceneScoring : ScoringTestScene + { + protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + + protected override IBeatmap CreateBeatmap(int maxCombo) + { + var beatmap = new OsuBeatmap(); + for (int i = 0; i < maxCombo; i++) + beatmap.HitObjects.Add(new HitCircle()); + return beatmap; + } + + protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }; + protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }; + protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }; + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs similarity index 53% rename from osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs rename to osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index 5db05ea67c..ecf55419a2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -14,15 +14,13 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; -using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Osu.Beatmaps; -using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring.Legacy; using osuTK; @@ -31,8 +29,14 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public partial class TestSceneScoring : OsuTestScene + public abstract partial class ScoringTestScene : OsuTestScene { + protected abstract ScoreProcessor CreateScoreProcessor(); + protected abstract IBeatmap CreateBeatmap(int maxCombo); + protected abstract JudgementResult CreatePerfectJudgementResult(); + protected abstract JudgementResult CreateNonPerfectJudgementResult(); + protected abstract JudgementResult CreateMissJudgementResult(); + private GraphContainer graphs = null!; private SettingsSlider sliderMaxCombo = null!; private SettingsCheckbox scaleToMax = null!; @@ -152,8 +156,8 @@ namespace osu.Game.Tests.Visual.Gameplay graphs.Clear(); legend.Clear(); - runForProcessor("lazer-standardised", colours.Green1, new OsuScoreProcessor(), ScoringMode.Standardised, standardisedVisible); - runForProcessor("lazer-classic", colours.Blue1, new OsuScoreProcessor(), ScoringMode.Classic, classicVisible); + runForProcessor("lazer-standardised", colours.Green1, CreateScoreProcessor(), ScoringMode.Standardised, standardisedVisible); + runForProcessor("lazer-classic", colours.Blue1, CreateScoreProcessor(), ScoringMode.Classic, classicVisible); runScoreV1(); runScoreV2(); @@ -274,20 +278,16 @@ namespace osu.Game.Tests.Visual.Gameplay private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode, BindableBool visibility) { int maxCombo = sliderMaxCombo.Current.Value; - - var beatmap = new OsuBeatmap(); - for (int i = 0; i < maxCombo; i++) - beatmap.HitObjects.Add(new HitCircle()); - + var beatmap = CreateBeatmap(maxCombo); processor.ApplyBeatmap(beatmap); runForAlgorithm(new ScoringAlgorithm { Name = name, Colour = colour, - ApplyHit = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }), - ApplyNonPerfect = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }), - ApplyMiss = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }), + ApplyHit = () => processor.ApplyResult(CreatePerfectJudgementResult()), + ApplyNonPerfect = () => processor.ApplyResult(CreateNonPerfectJudgementResult()), + ApplyMiss = () => processor.ApplyResult(CreateMissJudgementResult()), GetTotalScore = () => processor.GetDisplayScore(mode), Visible = visibility }); @@ -327,310 +327,310 @@ namespace osu.Game.Tests.Visual.Gameplay AccentColour = scoringAlgorithm.Colour, }); } - } - public class ScoringAlgorithm - { - public string Name { get; init; } = null!; - public Color4 Colour { get; init; } - public Action ApplyHit { get; init; } = () => { }; - public Action ApplyNonPerfect { get; init; } = () => { }; - public Action ApplyMiss { get; init; } = () => { }; - public Func GetTotalScore { get; init; } = null!; - public BindableBool Visible { get; init; } = null!; - } - - public partial class GraphContainer : Container, IHasCustomTooltip> - { - public readonly BindableList MissLocations = new BindableList(); - public readonly BindableList NonPerfectLocations = new BindableList(); - - public Bindable MaxCombo = new Bindable(); - - protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - - private readonly Box hoverLine; - - private readonly Container missLines; - private readonly Container verticalGridLines; - - public int CurrentHoverCombo { get; private set; } - - public GraphContainer() + private class ScoringAlgorithm { - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.1f), - RelativeSizeAxes = Axes.Both, - }, - verticalGridLines = new Container - { - RelativeSizeAxes = Axes.Both, - }, - hoverLine = new Box - { - Colour = Color4.Yellow, - RelativeSizeAxes = Axes.Y, - Origin = Anchor.TopCentre, - Alpha = 0, - Width = 1, - }, - missLines = new Container - { - Alpha = 0.6f, - RelativeSizeAxes = Axes.Both, - }, - Content, - } - }; - - MissLocations.BindCollectionChanged((_, _) => updateMissLocations()); - NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations()); - - MaxCombo.BindValueChanged(_ => - { - updateMissLocations(); - updateVerticalGridLines(); - }, true); + public string Name { get; init; } = null!; + public Color4 Colour { get; init; } + public Action ApplyHit { get; init; } = () => { }; + public Action ApplyNonPerfect { get; init; } = () => { }; + public Action ApplyMiss { get; init; } = () => { }; + public Func GetTotalScore { get; init; } = null!; + public BindableBool Visible { get; init; } = null!; } - private void updateVerticalGridLines() + public partial class GraphContainer : Container, IHasCustomTooltip> { - verticalGridLines.Clear(); + public readonly BindableList MissLocations = new BindableList(); + public readonly BindableList NonPerfectLocations = new BindableList(); - for (int i = 0; i < MaxCombo.Value; i++) + public Bindable MaxCombo = new Bindable(); + + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + + private readonly Box hoverLine; + + private readonly Container missLines; + private readonly Container verticalGridLines; + + public int CurrentHoverCombo { get; private set; } + + public GraphContainer() { - if (i % 100 == 0) + InternalChild = new Container { - verticalGridLines.AddRange(new Drawable[] + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { new Box { - Colour = OsuColour.Gray(0.2f), - Origin = Anchor.TopCentre, - Width = 1, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - X = (float)i / MaxCombo.Value, + Colour = OsuColour.Gray(0.1f), + RelativeSizeAxes = Axes.Both, }, - new OsuSpriteText + verticalGridLines = new Container { - RelativePositionAxes = Axes.X, - X = (float)i / MaxCombo.Value, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Text = $"{i:#,0}", - Rotation = -30, - Y = -20, - } + RelativeSizeAxes = Axes.Both, + }, + hoverLine = new Box + { + Colour = Color4.Yellow, + RelativeSizeAxes = Axes.Y, + Origin = Anchor.TopCentre, + Alpha = 0, + Width = 1, + }, + missLines = new Container + { + Alpha = 0.6f, + RelativeSizeAxes = Axes.Both, + }, + Content, + } + }; + + MissLocations.BindCollectionChanged((_, _) => updateMissLocations()); + NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations()); + + MaxCombo.BindValueChanged(_ => + { + updateMissLocations(); + updateVerticalGridLines(); + }, true); + } + + private void updateVerticalGridLines() + { + verticalGridLines.Clear(); + + for (int i = 0; i < MaxCombo.Value; i++) + { + if (i % 100 == 0) + { + verticalGridLines.AddRange(new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.2f), + Origin = Anchor.TopCentre, + Width = 1, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + X = (float)i / MaxCombo.Value, + }, + new OsuSpriteText + { + RelativePositionAxes = Axes.X, + X = (float)i / MaxCombo.Value, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = $"{i:#,0}", + Rotation = -30, + Y = -20, + } + }); + } + } + } + + private void updateMissLocations() + { + missLines.Clear(); + + foreach (int miss in MissLocations) + { + missLines.Add(new Box + { + Colour = Color4.Red, + Origin = Anchor.TopCentre, + Width = 1, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + X = (float)miss / MaxCombo.Value, + }); + } + + foreach (int miss in NonPerfectLocations) + { + missLines.Add(new Box + { + Colour = Color4.Orange, + Origin = Anchor.TopCentre, + Width = 1, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + X = (float)miss / MaxCombo.Value, }); } } - } - private void updateMissLocations() - { - missLines.Clear(); - - foreach (int miss in MissLocations) + protected override bool OnHover(HoverEvent e) { - missLines.Add(new Box - { - Colour = Color4.Red, - Origin = Anchor.TopCentre, - Width = 1, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - X = (float)miss / MaxCombo.Value, - }); + hoverLine.Show(); + return base.OnHover(e); } - foreach (int miss in NonPerfectLocations) + protected override void OnHoverLost(HoverLostEvent e) { - missLines.Add(new Box - { - Colour = Color4.Orange, - Origin = Anchor.TopCentre, - Width = 1, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - X = (float)miss / MaxCombo.Value, - }); + hoverLine.Hide(); + base.OnHoverLost(e); } - } - protected override bool OnHover(HoverEvent e) - { - hoverLine.Show(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - hoverLine.Hide(); - base.OnHoverLost(e); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value); - - hoverLine.X = e.MousePosition.X; - return base.OnMouseMove(e); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - if (e.Button == MouseButton.Left) - MissLocations.Add(CurrentHoverCombo); - else - NonPerfectLocations.Add(CurrentHoverCombo); - - return true; - } - - private GraphTooltip? tooltip; - - public ITooltip> GetCustomTooltip() => tooltip ??= new GraphTooltip(this); - - public IEnumerable TooltipContent => Content; - - public partial class GraphTooltip : CompositeDrawable, ITooltip> - { - private readonly GraphContainer graphContainer; - - private readonly OsuTextFlowContainer textFlow; - - public GraphTooltip(GraphContainer graphContainer) + protected override bool OnMouseMove(MouseMoveEvent e) { - this.graphContainer = graphContainer; - AutoSizeAxes = Axes.Both; + CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value); - Masking = true; - CornerRadius = 10; + hoverLine.X = e.MousePosition.X; + return base.OnMouseMove(e); + } - InternalChildren = new Drawable[] + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + MissLocations.Add(CurrentHoverCombo); + else + NonPerfectLocations.Add(CurrentHoverCombo); + + return true; + } + + private GraphTooltip? tooltip; + + public ITooltip> GetCustomTooltip() => tooltip ??= new GraphTooltip(this); + + public IEnumerable TooltipContent => Content; + + public partial class GraphTooltip : CompositeDrawable, ITooltip> + { + private readonly GraphContainer graphContainer; + + private readonly OsuTextFlowContainer textFlow; + + public GraphTooltip(GraphContainer graphContainer) { - new Box + this.graphContainer = graphContainer; + AutoSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = 10; + + InternalChildren = new Drawable[] { - Colour = OsuColour.Gray(0.15f), - RelativeSizeAxes = Axes.Both, + new Box + { + Colour = OsuColour.Gray(0.15f), + RelativeSizeAxes = Axes.Both, + }, + textFlow = new OsuTextFlowContainer + { + Colour = Color4.White, + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + } + }; + } + + private int? lastContentCombo; + + public void SetContent(IEnumerable content) + { + int relevantCombo = graphContainer.CurrentHoverCombo; + + if (lastContentCombo == relevantCombo) + return; + + lastContentCombo = relevantCombo; + textFlow.Clear(); + + textFlow.AddParagraph($"At combo {relevantCombo}:"); + + foreach (var graph in content) + { + if (graph.Alpha == 0) continue; + + float valueAtHover = graph.Values.ElementAt(relevantCombo); + float ofTotal = valueAtHover / graph.Values.Last(); + + textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour); + } + } + + public void Move(Vector2 pos) => this.MoveTo(pos); + } + } + + private partial class LegendEntry : OsuClickableContainer, IHasAccentColour + { + public Color4 AccentColour { get; set; } + + public BindableBool Visible { get; } = new BindableBool(true); + + public readonly long FinalScore; + + private readonly string description; + private readonly LineGraph lineGraph; + + private OsuSpriteText descriptionText = null!; + private OsuSpriteText finalScoreText = null!; + + public LegendEntry(ScoringAlgorithm scoringAlgorithm, LineGraph lineGraph) + { + description = scoringAlgorithm.Name; + FinalScore = scoringAlgorithm.GetTotalScore(); + AccentColour = scoringAlgorithm.Colour; + Visible.BindTo(scoringAlgorithm.Visible); + + this.lineGraph = lineGraph; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X; + AutoSizeAxes = Content.AutoSizeAxes = Axes.Y; + + Children = new Drawable[] + { + descriptionText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }, - textFlow = new OsuTextFlowContainer + finalScoreText = new OsuSpriteText { - Colour = Color4.White, - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(10), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.Default.With(fixedWidth: true) } }; } - private int? lastContentCombo; - - public void SetContent(IEnumerable content) + protected override void LoadComplete() { - int relevantCombo = graphContainer.CurrentHoverCombo; - - if (lastContentCombo == relevantCombo) - return; - - lastContentCombo = relevantCombo; - textFlow.Clear(); - - textFlow.AddParagraph($"At combo {relevantCombo}:"); - - foreach (var graph in content) - { - if (graph.Alpha == 0) continue; - - float valueAtHover = graph.Values.ElementAt(relevantCombo); - float ofTotal = valueAtHover / graph.Values.Last(); - - textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour); - } + base.LoadComplete(); + Visible.BindValueChanged(_ => updateState(), true); + Action = Visible.Toggle; } - public void Move(Vector2 pos) => this.MoveTo(pos); - } - } - - public partial class LegendEntry : OsuClickableContainer, IHasAccentColour - { - public Color4 AccentColour { get; set; } - - public BindableBool Visible { get; } = new BindableBool(true); - - public readonly long FinalScore; - - private readonly string description; - private readonly LineGraph lineGraph; - - private OsuSpriteText descriptionText = null!; - private OsuSpriteText finalScoreText = null!; - - public LegendEntry(ScoringAlgorithm scoringAlgorithm, LineGraph lineGraph) - { - description = scoringAlgorithm.Name; - FinalScore = scoringAlgorithm.GetTotalScore(); - AccentColour = scoringAlgorithm.Colour; - Visible.BindTo(scoringAlgorithm.Visible); - - this.lineGraph = lineGraph; - } - - [BackgroundDependencyLoader] - private void load() - { - RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X; - AutoSizeAxes = Content.AutoSizeAxes = Axes.Y; - - Children = new Drawable[] + protected override bool OnHover(HoverEvent e) { - descriptionText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - finalScoreText = new OsuSpriteText - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Font = OsuFont.Default.With(fixedWidth: true) - } - }; - } + updateState(); + return true; + } - protected override void LoadComplete() - { - base.LoadComplete(); - Visible.BindValueChanged(_ => updateState(), true); - Action = Visible.Toggle; - } + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } - protected override bool OnHover(HoverEvent e) - { - updateState(); - return true; - } + private void updateState() + { + Colour = IsHovered ? AccentColour.Lighten(0.2f) : AccentColour; - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - private void updateState() - { - Colour = IsHovered ? AccentColour.Lighten(0.2f) : AccentColour; - - descriptionText.Text = $"{(Visible.Value ? FontAwesome.Solid.CheckCircle.Icon : FontAwesome.Solid.Circle.Icon)} {description}"; - finalScoreText.Text = FinalScore.ToString("#,0"); - lineGraph.Alpha = Visible.Value ? 1 : 0; + descriptionText.Text = $"{(Visible.Value ? FontAwesome.Solid.CheckCircle.Icon : FontAwesome.Solid.Circle.Icon)} {description}"; + finalScoreText.Text = FinalScore.ToString("#,0"); + lineGraph.Alpha = Visible.Value ? 1 : 0; + } } } } From 27b6bc3062cc7e531677e914ffd762671089555a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Sep 2023 14:35:07 +0200 Subject: [PATCH 31/71] Add skeleton of catch scoring test --- .../TestSceneScoring.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..7330e40568 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs @@ -0,0 +1,91 @@ +// 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Scoring; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual.Gameplay; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public partial class TestSceneScoring : ScoringTestScene + { + protected override IBeatmap CreateBeatmap(int maxCombo) + { + var beatmap = new CatchBeatmap(); + for (int i = 0; i < maxCombo; ++i) + beatmap.HitObjects.Add(new Fruit()); + return beatmap; + } + + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(); + + protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); + + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new CatchProcessorBasedScoringAlgorithm(beatmap, mode); + + private class ScoreV1 : IScoringAlgorithm + { + public void ApplyHit() + { + } + + public void ApplyNonPerfect() + { + } + + public void ApplyMiss() + { + } + + public long TotalScore => 0; + } + + private class ScoreV2 : IScoringAlgorithm + { + private readonly int maxCombo; + + public ScoreV2(int maxCombo) + { + this.maxCombo = maxCombo; + } + + public void ApplyHit() + { + } + + public void ApplyNonPerfect() + { + } + + public void ApplyMiss() + { + } + + public long TotalScore => 0; + } + + private class CatchProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public CatchProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + : base(beatmap, mode) + { + } + + protected override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); + + protected override JudgementResult CreatePerfectJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Great }; + + protected override JudgementResult CreateNonPerfectJudgementResult() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements."); + + protected override JudgementResult CreateMissJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Miss }; + } + } +} From 0c22ff2a8093efe5754f060dbab424af81571c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Sep 2023 10:13:53 +0200 Subject: [PATCH 32/71] Refactor further to allow extensibility to other rulesets --- .../TestSceneScoring.cs | 118 +++++++++- .../Tests/Visual/Gameplay/ScoringTestScene.cs | 207 +++++++----------- 2 files changed, 187 insertions(+), 138 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs index 5dbfc8d3a1..36485b93ab 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; @@ -16,8 +17,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public partial class TestSceneScoring : ScoringTestScene { - protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); - protected override IBeatmap CreateBeatmap(int maxCombo) { var beatmap = new OsuBeatmap(); @@ -26,8 +25,117 @@ namespace osu.Game.Rulesets.Osu.Tests return beatmap; } - protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }; - protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }; - protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }; + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(); + protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new OsuProcessorBasedScoringAlgorithm(beatmap, mode); + + private const int base_great = 300; + private const int base_ok = 100; + + private class ScoreV1 : IScoringAlgorithm + { + private int currentCombo; + + // this corresponds to stable's `ScoreMultiplier`. + // value is chosen arbitrarily, towards the upper range. + private const float score_multiplier = 4; + + public void ApplyHit() => applyHitV1(base_great); + public void ApplyNonPerfect() => applyHitV1(base_ok); + public void ApplyMiss() => applyHitV1(0); + + private void applyHitV1(int baseScore) + { + if (baseScore == 0) + { + currentCombo = 0; + return; + } + + TotalScore += baseScore; + + // combo multiplier + // ReSharper disable once PossibleLossOfFraction + TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier)); + + currentCombo++; + } + + public long TotalScore { get; private set; } + } + + private class ScoreV2 : IScoringAlgorithm + { + private int currentCombo; + private double comboPortion; + private double currentBaseScore; + private double maxBaseScore; + private int currentHits; + + private readonly double comboPortionMax; + private readonly int maxCombo; + + public ScoreV2(int maxCombo) + { + this.maxCombo = maxCombo; + + for (int i = 0; i < this.maxCombo; i++) + ApplyHit(); + + comboPortionMax = comboPortion; + + currentCombo = 0; + comboPortion = 0; + currentBaseScore = 0; + maxBaseScore = 0; + currentHits = 0; + } + + public void ApplyHit() => applyHitV2(base_great); + public void ApplyNonPerfect() => applyHitV2(base_ok); + + private void applyHitV2(int baseScore) + { + maxBaseScore += base_great; + currentBaseScore += baseScore; + comboPortion += baseScore * (1 + ++currentCombo / 10.0); + + currentHits++; + } + + public void ApplyMiss() + { + currentHits++; + maxBaseScore += base_great; + currentCombo = 0; + } + + public long TotalScore + { + get + { + double accuracy = currentBaseScore / maxBaseScore; + + return (int)Math.Round + ( + 700000 * comboPortion / comboPortionMax + + 300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo) + ); + } + } + } + + private class OsuProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public OsuProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + : base(beatmap, mode) + { + } + + protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }; + protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }; + protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }; + } } } diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index ecf55419a2..331f1bb9aa 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.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; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -31,11 +30,11 @@ namespace osu.Game.Tests.Visual.Gameplay { public abstract partial class ScoringTestScene : OsuTestScene { - protected abstract ScoreProcessor CreateScoreProcessor(); protected abstract IBeatmap CreateBeatmap(int maxCombo); - protected abstract JudgementResult CreatePerfectJudgementResult(); - protected abstract JudgementResult CreateNonPerfectJudgementResult(); - protected abstract JudgementResult CreateMissJudgementResult(); + + protected abstract IScoringAlgorithm CreateScoreV1(); + protected abstract IScoringAlgorithm CreateScoreV2(int maxCombo); + protected abstract ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode); private GraphContainer graphs = null!; private SettingsSlider sliderMaxCombo = null!; @@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = $"Left click to add miss\nRight click to add OK/{base_ok}", + Text = "Left click to add miss\nRight click to add OK", Margin = new MarginPadding { Top = 20 } } } @@ -148,19 +147,28 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private const int base_great = 300; - private const int base_ok = 100; - private void rerun() { graphs.Clear(); legend.Clear(); - runForProcessor("lazer-standardised", colours.Green1, CreateScoreProcessor(), ScoringMode.Standardised, standardisedVisible); - runForProcessor("lazer-classic", colours.Blue1, CreateScoreProcessor(), ScoringMode.Classic, classicVisible); + runForProcessor("lazer-standardised", colours.Green1, ScoringMode.Standardised, standardisedVisible); + runForProcessor("lazer-classic", colours.Blue1, ScoringMode.Classic, classicVisible); - runScoreV1(); - runScoreV2(); + runForAlgorithm(new ScoringAlgorithmInfo + { + Name = "ScoreV1 (classic)", + Colour = colours.Purple1, + Algorithm = CreateScoreV1(), + Visible = scoreV1Visible + }); + runForAlgorithm(new ScoringAlgorithmInfo + { + Name = "ScoreV2", + Colour = colours.Red1, + Algorithm = CreateScoreV2(sliderMaxCombo.Current.Value), + Visible = scoreV2Visible + }); rescalePlots(); } @@ -181,119 +189,22 @@ namespace osu.Game.Tests.Visual.Gameplay } } - private void runScoreV1() - { - int totalScore = 0; - int currentCombo = 0; - - void applyHitV1(int baseScore) - { - if (baseScore == 0) - { - currentCombo = 0; - return; - } - - // this corresponds to stable's `ScoreMultiplier`. - // value is chosen arbitrarily, towards the upper range. - const float score_multiplier = 4; - - totalScore += baseScore; - - // combo multiplier - // ReSharper disable once PossibleLossOfFraction - totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier)); - - currentCombo++; - } - - runForAlgorithm(new ScoringAlgorithm - { - Name = "ScoreV1 (classic)", - Colour = colours.Purple1, - ApplyHit = () => applyHitV1(base_great), - ApplyNonPerfect = () => applyHitV1(base_ok), - ApplyMiss = () => applyHitV1(0), - GetTotalScore = () => totalScore, - Visible = scoreV1Visible - }); - } - - private void runScoreV2() - { - int maxCombo = sliderMaxCombo.Current.Value; - - int currentCombo = 0; - double comboPortion = 0; - double currentBaseScore = 0; - double maxBaseScore = 0; - int currentHits = 0; - - for (int i = 0; i < maxCombo; i++) - applyHitV2(base_great); - - double comboPortionMax = comboPortion; - - currentCombo = 0; - comboPortion = 0; - currentBaseScore = 0; - maxBaseScore = 0; - currentHits = 0; - - void applyHitV2(int baseScore) - { - maxBaseScore += base_great; - currentBaseScore += baseScore; - comboPortion += baseScore * (1 + ++currentCombo / 10.0); - - currentHits++; - } - - runForAlgorithm(new ScoringAlgorithm - { - Name = "ScoreV2", - Colour = colours.Red1, - ApplyHit = () => applyHitV2(base_great), - ApplyNonPerfect = () => applyHitV2(base_ok), - ApplyMiss = () => - { - currentHits++; - maxBaseScore += base_great; - currentCombo = 0; - }, - GetTotalScore = () => - { - double accuracy = currentBaseScore / maxBaseScore; - - return (int)Math.Round - ( - 700000 * comboPortion / comboPortionMax + - 300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo) - ); - }, - Visible = scoreV2Visible - }); - } - - private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode, BindableBool visibility) + private void runForProcessor(string name, Color4 colour, ScoringMode scoringMode, BindableBool visibility) { int maxCombo = sliderMaxCombo.Current.Value; var beatmap = CreateBeatmap(maxCombo); - processor.ApplyBeatmap(beatmap); + var algorithm = CreateScoreAlgorithm(beatmap, scoringMode); - runForAlgorithm(new ScoringAlgorithm + runForAlgorithm(new ScoringAlgorithmInfo { Name = name, Colour = colour, - ApplyHit = () => processor.ApplyResult(CreatePerfectJudgementResult()), - ApplyNonPerfect = () => processor.ApplyResult(CreateNonPerfectJudgementResult()), - ApplyMiss = () => processor.ApplyResult(CreateMissJudgementResult()), - GetTotalScore = () => processor.GetDisplayScore(mode), + Algorithm = algorithm, Visible = visibility }); } - private void runForAlgorithm(ScoringAlgorithm scoringAlgorithm) + private void runForAlgorithm(ScoringAlgorithmInfo algorithmInfo) { int maxCombo = sliderMaxCombo.Current.Value; @@ -302,43 +213,73 @@ namespace osu.Game.Tests.Visual.Gameplay for (int i = 0; i < maxCombo; i++) { if (graphs.MissLocations.Contains(i)) - scoringAlgorithm.ApplyMiss(); + algorithmInfo.Algorithm.ApplyMiss(); else if (graphs.NonPerfectLocations.Contains(i)) - scoringAlgorithm.ApplyNonPerfect(); + algorithmInfo.Algorithm.ApplyNonPerfect(); else - scoringAlgorithm.ApplyHit(); + algorithmInfo.Algorithm.ApplyHit(); - results.Add(scoringAlgorithm.GetTotalScore()); + results.Add(algorithmInfo.Algorithm.TotalScore); } LineGraph graph; graphs.Add(graph = new LineGraph { - Name = scoringAlgorithm.Name, + Name = algorithmInfo.Name, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - LineColour = scoringAlgorithm.Colour, + LineColour = algorithmInfo.Colour, Values = results }); - legend.Add(new LegendEntry(scoringAlgorithm, graph) + legend.Add(new LegendEntry(algorithmInfo, graph) { - AccentColour = scoringAlgorithm.Colour, + AccentColour = algorithmInfo.Colour, }); } - private class ScoringAlgorithm + private class ScoringAlgorithmInfo { public string Name { get; init; } = null!; public Color4 Colour { get; init; } - public Action ApplyHit { get; init; } = () => { }; - public Action ApplyNonPerfect { get; init; } = () => { }; - public Action ApplyMiss { get; init; } = () => { }; - public Func GetTotalScore { get; init; } = null!; + public IScoringAlgorithm Algorithm { get; init; } = null!; public BindableBool Visible { get; init; } = null!; } + protected interface IScoringAlgorithm + { + void ApplyHit(); + void ApplyNonPerfect(); + void ApplyMiss(); + + long TotalScore { get; } + } + + protected abstract class ProcessorBasedScoringAlgorithm : IScoringAlgorithm + { + protected abstract ScoreProcessor CreateScoreProcessor(); + protected abstract JudgementResult CreatePerfectJudgementResult(); + protected abstract JudgementResult CreateNonPerfectJudgementResult(); + protected abstract JudgementResult CreateMissJudgementResult(); + + private readonly ScoreProcessor scoreProcessor; + private readonly ScoringMode mode; + + protected ProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + { + this.mode = mode; + scoreProcessor = CreateScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + } + + public void ApplyHit() => scoreProcessor.ApplyResult(CreatePerfectJudgementResult()); + public void ApplyNonPerfect() => scoreProcessor.ApplyResult(CreateNonPerfectJudgementResult()); + public void ApplyMiss() => scoreProcessor.ApplyResult(CreateMissJudgementResult()); + + public long TotalScore => scoreProcessor.GetDisplayScore(mode); + } + public partial class GraphContainer : Container, IHasCustomTooltip> { public readonly BindableList MissLocations = new BindableList(); @@ -572,12 +513,12 @@ namespace osu.Game.Tests.Visual.Gameplay private OsuSpriteText descriptionText = null!; private OsuSpriteText finalScoreText = null!; - public LegendEntry(ScoringAlgorithm scoringAlgorithm, LineGraph lineGraph) + public LegendEntry(ScoringAlgorithmInfo scoringAlgorithmInfo, LineGraph lineGraph) { - description = scoringAlgorithm.Name; - FinalScore = scoringAlgorithm.GetTotalScore(); - AccentColour = scoringAlgorithm.Colour; - Visible.BindTo(scoringAlgorithm.Visible); + description = scoringAlgorithmInfo.Name; + FinalScore = scoringAlgorithmInfo.Algorithm.TotalScore; + AccentColour = scoringAlgorithmInfo.Colour; + Visible.BindTo(scoringAlgorithmInfo.Visible); this.lineGraph = lineGraph; } From 5eccc771c2f3a7b26536366f3766e7f1246ff73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Sep 2023 14:40:24 +0200 Subject: [PATCH 33/71] Turn off non-perfect judgements for catch scoring test scene --- .../TestSceneScoring.cs | 5 ++++ .../Tests/Visual/Gameplay/ScoringTestScene.cs | 25 +++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs index 7330e40568..44823607b8 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs @@ -17,6 +17,11 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public partial class TestSceneScoring : ScoringTestScene { + public TestSceneScoring() + : base(supportsNonPerfectJudgements: false) + { + } + protected override IBeatmap CreateBeatmap(int maxCombo) { var beatmap = new CatchBeatmap(); diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index c213a17185..de4688a6fe 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -40,6 +40,8 @@ namespace osu.Game.Tests.Visual.Gameplay protected BindableList NonPerfectLocations => graphs.NonPerfectLocations; protected BindableList MissLocations => graphs.MissLocations; + private readonly bool supportsNonPerfectJudgements; + private GraphContainer graphs = null!; private SettingsSlider sliderMaxCombo = null!; private SettingsCheckbox scaleToMax = null!; @@ -54,11 +56,18 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuColour colours { get; set; } = null!; + protected ScoringTestScene(bool supportsNonPerfectJudgements = true) + { + this.supportsNonPerfectJudgements = supportsNonPerfectJudgements; + } + [SetUpSteps] public void SetUpSteps() { AddStep("setup tests", () => { + OsuTextFlowContainer clickExplainer; + Children = new Drawable[] { new Box @@ -79,7 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay { new Drawable[] { - graphs = new GraphContainer + graphs = new GraphContainer(supportsNonPerfectJudgements) { RelativeSizeAxes = Axes.Both, }, @@ -120,11 +129,10 @@ namespace osu.Game.Tests.Visual.Gameplay LabelText = "Rescale plots to 100%", Current = { Value = true, Default = true } }, - new OsuTextFlowContainer + clickExplainer = new OsuTextFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = "Left click to add miss\nRight click to add OK", Margin = new MarginPadding { Top = 20 } } } @@ -134,6 +142,10 @@ namespace osu.Game.Tests.Visual.Gameplay } }; + clickExplainer.AddParagraph("Left click to add miss"); + if (supportsNonPerfectJudgements) + clickExplainer.AddParagraph("Right click to add OK"); + sliderMaxCombo.Current.BindValueChanged(_ => Rerun()); scaleToMax.Current.BindValueChanged(_ => Rerun()); @@ -286,6 +298,8 @@ namespace osu.Game.Tests.Visual.Gameplay public partial class GraphContainer : Container, IHasCustomTooltip> { + private readonly bool supportsNonPerfectJudgements; + public readonly BindableList MissLocations = new BindableList(); public readonly BindableList NonPerfectLocations = new BindableList(); @@ -300,8 +314,9 @@ namespace osu.Game.Tests.Visual.Gameplay public int CurrentHoverCombo { get; private set; } - public GraphContainer() + public GraphContainer(bool supportsNonPerfectJudgements) { + this.supportsNonPerfectJudgements = supportsNonPerfectJudgements; InternalChild = new Container { RelativeSizeAxes = Axes.Both, @@ -432,7 +447,7 @@ namespace osu.Game.Tests.Visual.Gameplay { if (e.Button == MouseButton.Left) MissLocations.Add(CurrentHoverCombo); - else + else if (supportsNonPerfectJudgements) NonPerfectLocations.Add(CurrentHoverCombo); return true; From ebdc501e5b6f3dbea0af959ca4fdbb96e3b3ba13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Sep 2023 14:19:49 +0200 Subject: [PATCH 34/71] Add example scenarios and configurable score multiplier --- .../TestSceneScoring.cs | 45 ++++++++++++++++--- .../Tests/Visual/Gameplay/ScoringTestScene.cs | 22 +++++---- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs index 36485b93ab..bb09328ab7 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs @@ -3,6 +3,7 @@ using System; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Beatmaps; @@ -17,6 +18,12 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public partial class TestSceneScoring : ScoringTestScene { + private Bindable scoreMultiplier { get; } = new BindableDouble + { + Default = 4, + Value = 4 + }; + protected override IBeatmap CreateBeatmap(int maxCombo) { var beatmap = new OsuBeatmap(); @@ -25,10 +32,40 @@ namespace osu.Game.Rulesets.Osu.Tests return beatmap; } - protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(); + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } }; protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new OsuProcessorBasedScoringAlgorithm(beatmap, mode); + [Test] + public void TestBasicScenarios() + { + AddStep("set up score multiplier", () => + { + scoreMultiplier.BindValueChanged(_ => Rerun()); + }); + AddStep("set max combo to 100", () => MaxCombo.Value = 100); + AddStep("set perfect score", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + }); + AddStep("set score with misses", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddStep("set score with misses and OKs", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + + NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 }); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier); + } + private const int base_great = 300; private const int base_ok = 100; @@ -36,9 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests { private int currentCombo; - // this corresponds to stable's `ScoreMultiplier`. - // value is chosen arbitrarily, towards the upper range. - private const float score_multiplier = 4; + public BindableDouble ScoreMultiplier { get; } = new BindableDouble(); public void ApplyHit() => applyHitV1(base_great); public void ApplyNonPerfect() => applyHitV1(base_ok); @@ -56,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Tests // combo multiplier // ReSharper disable once PossibleLossOfFraction - TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier)); + TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value)); currentCombo++; } diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index 331f1bb9aa..c213a17185 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -13,6 +12,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -36,6 +36,10 @@ namespace osu.Game.Tests.Visual.Gameplay protected abstract IScoringAlgorithm CreateScoreV2(int maxCombo); protected abstract ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode); + protected Bindable MaxCombo => sliderMaxCombo.Current; + protected BindableList NonPerfectLocations => graphs.NonPerfectLocations; + protected BindableList MissLocations => graphs.MissLocations; + private GraphContainer graphs = null!; private SettingsSlider sliderMaxCombo = null!; private SettingsCheckbox scaleToMax = null!; @@ -50,8 +54,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuColour colours { get; set; } = null!; - [Test] - public void TestBasic() + [SetUpSteps] + public void SetUpSteps() { AddStep("setup tests", () => { @@ -130,24 +134,24 @@ namespace osu.Game.Tests.Visual.Gameplay } }; - sliderMaxCombo.Current.BindValueChanged(_ => rerun()); - scaleToMax.Current.BindValueChanged(_ => rerun()); + sliderMaxCombo.Current.BindValueChanged(_ => Rerun()); + scaleToMax.Current.BindValueChanged(_ => Rerun()); standardisedVisible.BindValueChanged(_ => rescalePlots()); classicVisible.BindValueChanged(_ => rescalePlots()); scoreV1Visible.BindValueChanged(_ => rescalePlots()); scoreV2Visible.BindValueChanged(_ => rescalePlots()); - graphs.MissLocations.BindCollectionChanged((_, __) => rerun()); - graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun()); + graphs.MissLocations.BindCollectionChanged((_, __) => Rerun()); + graphs.NonPerfectLocations.BindCollectionChanged((_, __) => Rerun()); graphs.MaxCombo.BindTo(sliderMaxCombo.Current); - rerun(); + Rerun(); }); } - private void rerun() + protected void Rerun() { graphs.Clear(); legend.Clear(); From c6445a327b1fa76d40b5629a8fa72f7abf9c8836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Sep 2023 13:14:01 +0200 Subject: [PATCH 35/71] Add taiko scoring test scene --- .../TestSceneScoring.cs | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..e065070822 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs @@ -0,0 +1,185 @@ +// 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 NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; +using osu.Game.Tests.Visual.Gameplay; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public partial class TestSceneScoring : ScoringTestScene + { + private Bindable scoreMultiplier { get; } = new BindableDouble + { + Default = 4, + Value = 4 + }; + + protected override IBeatmap CreateBeatmap(int maxCombo) + { + var beatmap = new TaikoBeatmap(); + for (int i = 0; i < maxCombo; ++i) + beatmap.HitObjects.Add(new Hit()); + return beatmap; + } + + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } }; + protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); + + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new TaikoProcessorBasedScoringAlgorithm(beatmap, mode); + + [Test] + public void TestBasicScenarios() + { + AddStep("set up score multiplier", () => + { + scoreMultiplier.BindValueChanged(_ => Rerun()); + }); + AddStep("set max combo to 100", () => MaxCombo.Value = 100); + AddStep("set perfect score", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + }); + AddStep("set score with misses", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddStep("set score with misses and OKs", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + + NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 }); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier); + } + + private const int base_great = 300; + private const int base_ok = 150; + + private class ScoreV1 : IScoringAlgorithm + { + private int currentCombo; + + public BindableDouble ScoreMultiplier { get; } = new BindableDouble(); + + public void ApplyHit() => applyHitV1(base_great); + + public void ApplyNonPerfect() => applyHitV1(base_ok); + + public void ApplyMiss() => applyHitV1(0); + + private void applyHitV1(int baseScore) + { + if (baseScore == 0) + { + currentCombo = 0; + return; + } + + TotalScore += baseScore; + + // combo multiplier + // ReSharper disable once PossibleLossOfFraction + TotalScore += (int)((baseScore / 35) * 2 * (ScoreMultiplier.Value + 1)) * (Math.Min(100, currentCombo) / 10); + + currentCombo++; + } + + public long TotalScore { get; private set; } + } + + private class ScoreV2 : IScoringAlgorithm + { + private int currentCombo; + private double comboPortion; + private double currentBaseScore; + private double maxBaseScore; + private int currentHits; + + private readonly double comboPortionMax; + private readonly int maxCombo; + + private const double combo_base = 4; + + public ScoreV2(int maxCombo) + { + this.maxCombo = maxCombo; + + for (int i = 0; i < this.maxCombo; i++) + ApplyHit(); + + comboPortionMax = comboPortion; + + currentCombo = 0; + comboPortion = 0; + currentBaseScore = 0; + maxBaseScore = 0; + currentHits = 0; + } + + public void ApplyHit() => applyHitV2(base_great); + + public void ApplyNonPerfect() => applyHitV2(base_ok); + + private void applyHitV2(int baseScore) + { + maxBaseScore += base_great; + currentBaseScore += baseScore; + + currentHits++; + + // `base_great` is INTENTIONALLY used above here instead of `baseScore` + // see `BaseHitValue` override in `ScoreChangeTaiko` on stable + comboPortion += base_great * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base)); + } + + public void ApplyMiss() + { + currentHits++; + maxBaseScore += base_great; + currentCombo = 0; + } + + public long TotalScore + { + get + { + double accuracy = currentBaseScore / maxBaseScore; + + return (int)Math.Round + ( + 250000 * comboPortion / comboPortionMax + + 750000 * Math.Pow(accuracy, 3.6) * ((double)currentHits / maxCombo) + ); + } + } + } + + private class TaikoProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public TaikoProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + : base(beatmap, mode) + { + } + + protected override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); + protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }; + protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }; + protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }; + } + } +} From aa8aa14a57ff48bdc707d7221490482714380bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Sep 2023 15:27:35 +0200 Subject: [PATCH 36/71] Add catch scoring algorithms to test scene --- .../TestSceneScoring.cs | 97 +++++++++++++++---- 1 file changed, 79 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs index 44823607b8..dfdde0a325 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs @@ -3,6 +3,7 @@ using System; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Judgements; @@ -22,6 +23,12 @@ namespace osu.Game.Rulesets.Catch.Tests { } + private Bindable scoreMultiplier { get; } = new BindableDouble + { + Default = 4, + Value = 4 + }; + protected override IBeatmap CreateBeatmap(int maxCombo) { var beatmap = new CatchBeatmap(); @@ -30,51 +37,105 @@ namespace osu.Game.Rulesets.Catch.Tests return beatmap; } - protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(); + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } }; protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new CatchProcessorBasedScoringAlgorithm(beatmap, mode); + [Test] + public void TestBasicScenarios() + { + AddStep("set up score multiplier", () => + { + scoreMultiplier.BindValueChanged(_ => Rerun()); + }); + AddStep("set max combo to 100", () => MaxCombo.Value = 100); + AddStep("set perfect score", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + }); + AddStep("set score with misses", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier); + } + + private const int base_great = 300; + private class ScoreV1 : IScoringAlgorithm { - public void ApplyHit() + private int currentCombo; + + public BindableDouble ScoreMultiplier { get; } = new BindableDouble(); + + public void ApplyHit() => applyHitV1(base_great); + + public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements."); + + public void ApplyMiss() => applyHitV1(0); + + private void applyHitV1(int baseScore) { + if (baseScore == 0) + { + currentCombo = 0; + return; + } + + TotalScore += baseScore; + + // combo multiplier + // ReSharper disable once PossibleLossOfFraction + TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value)); + + currentCombo++; } - public void ApplyNonPerfect() - { - } - - public void ApplyMiss() - { - } - - public long TotalScore => 0; + public long TotalScore { get; private set; } } private class ScoreV2 : IScoringAlgorithm { - private readonly int maxCombo; + private int currentCombo; + private double comboPortion; + + private readonly double comboPortionMax; + + private const double combo_base = 4; + private const int combo_cap = 200; public ScoreV2(int maxCombo) { - this.maxCombo = maxCombo; + for (int i = 0; i < maxCombo; i++) + ApplyHit(); + + comboPortionMax = comboPortion; + + currentCombo = 0; + comboPortion = 0; } - public void ApplyHit() - { - } + public void ApplyHit() => applyHitV2(base_great); - public void ApplyNonPerfect() + public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements."); + + private void applyHitV2(int baseScore) { + comboPortion += baseScore * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(combo_cap, combo_base)); } public void ApplyMiss() { + currentCombo = 0; } - public long TotalScore => 0; + public long TotalScore + => (int)Math.Round(1000000 * comboPortion / comboPortionMax); // vast simplification, as we're not doing ticks here. } private class CatchProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm From 5640dd2f74b44b8a2a4c03b9eb22e94e3e7e4f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Sep 2023 20:06:59 +0200 Subject: [PATCH 37/71] Add mania scoring test scene --- .../TestSceneScoring.cs | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..ae3ea861ea --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs @@ -0,0 +1,175 @@ +// 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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual.Gameplay; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public partial class TestSceneScoring : ScoringTestScene + { + protected override IBeatmap CreateBeatmap(int maxCombo) + { + var beatmap = new ManiaBeatmap(new StageDefinition(5)); + for (int i = 0; i < maxCombo; ++i) + beatmap.HitObjects.Add(new Note()); + return beatmap; + } + + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(MaxCombo.Value); + protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new ManiaProcessorBasedScoringAlgorithm(beatmap, mode); + + [Test] + public void TestBasicScenarios() + { + AddStep("set max combo to 100", () => MaxCombo.Value = 100); + AddStep("set perfect score", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + }); + AddStep("set score with misses", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddStep("set score with misses and OKs", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + + NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 }); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + } + + private class ScoreV1 : IScoringAlgorithm + { + private int currentCombo; + private double comboAddition = 100; + private double totalScoreDouble; + private readonly double scoreMultiplier; + + public ScoreV1(int maxCombo) + { + scoreMultiplier = 500000d / maxCombo; + } + + public void ApplyHit() => applyHitV1(320, add => add + 2, 32); + public void ApplyNonPerfect() => applyHitV1(100, add => add - 24, 8); + public void ApplyMiss() => applyHitV1(0, _ => -56, 0); + + private void applyHitV1(int scoreIncrease, Func comboAdditionFunc, int delta) + { + comboAddition = comboAdditionFunc(comboAddition); + if (currentCombo != 0 && currentCombo % 384 == 0) + comboAddition = 100; + comboAddition = Math.Max(0, Math.Min(comboAddition, 100)); + double scoreIncreaseD = Math.Sqrt(comboAddition) * delta * scoreMultiplier / 320; + + TotalScore = (long)totalScoreDouble; + + scoreIncreaseD += scoreIncrease * scoreMultiplier / 320; + scoreIncrease = (int)scoreIncreaseD; + + TotalScore += scoreIncrease; + totalScoreDouble += scoreIncreaseD; + + if (scoreIncrease > 0) + currentCombo++; + } + + public long TotalScore { get; private set; } + } + + private class ScoreV2 : IScoringAlgorithm + { + private int currentCombo; + private double comboPortion; + private double currentBaseScore; + private double maxBaseScore; + private int currentHits; + + private readonly double comboPortionMax; + private readonly int maxCombo; + + private const double combo_base = 4; + + public ScoreV2(int maxCombo) + { + this.maxCombo = maxCombo; + + for (int i = 0; i < this.maxCombo; i++) + ApplyHit(); + + comboPortionMax = comboPortion; + + currentCombo = 0; + comboPortion = 0; + currentBaseScore = 0; + maxBaseScore = 0; + currentHits = 0; + } + + public void ApplyHit() => applyHitV2(305, 300); + public void ApplyNonPerfect() => applyHitV2(100, 100); + + private void applyHitV2(int hitValue, int baseHitValue) + { + maxBaseScore += 305; + currentBaseScore += hitValue; + comboPortion += baseHitValue * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base)); + + currentHits++; + } + + public void ApplyMiss() + { + currentHits++; + maxBaseScore += 305; + currentCombo = 0; + } + + public long TotalScore + { + get + { + float accuracy = (float)(currentBaseScore / maxBaseScore); + + return (int)Math.Round + ( + 200000 * comboPortion / comboPortionMax + + 800000 * Math.Pow(accuracy, 2 + 2 * accuracy) * ((double)currentHits / maxCombo) + ); + } + } + } + + private class ManiaProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public ManiaProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + : base(beatmap, mode) + { + } + + protected override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); + + protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Perfect }; + + protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Ok }; + + protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Miss }; + } + } +} From 56b5f52e838f39aeabc2209e0d765603e7ebc477 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 16 Sep 2023 14:31:33 +0900 Subject: [PATCH 38/71] Update all dependencies (except for Moq) --- ...u.Game.Rulesets.EmptyFreeform.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 4 ++-- ....Game.Rulesets.EmptyScrolling.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 4 ++-- osu.Desktop/osu.Desktop.csproj | 2 +- .../osu.Game.Benchmarks.csproj | 4 ++-- .../osu.Game.Rulesets.Catch.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Mania.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Osu.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Taiko.Tests.csproj | 4 ++-- osu.Game.Tests/osu.Game.Tests.csproj | 4 ++-- .../osu.Game.Tournament.Tests.csproj | 4 ++-- .../API/ModSettingsDictionaryFormatter.cs | 4 ++-- .../Multiplayer/TestMultiplayerClient.cs | 2 +- osu.Game/osu.Game.csproj | 20 +++++++++---------- 15 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index a1c53ece03..2baa7ee0e0 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 683e9fd5e8..a2308e6dfc 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index b7a7fff18a..e839d2657c 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 683e9fd5e8..a2308e6dfc 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -9,9 +9,9 @@ false - + - + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 25f4cff00e..1d43e118a3 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 4719d54138..febe353b81 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -7,9 +7,9 @@ - + - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 01922b2a96..c45c85833c 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index 027bf60a0c..b991db408c 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 57900bffd7..ea033cda45 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -1,10 +1,10 @@  - + - + WinExe diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 0c39ad988b..48465bb119 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -1,9 +1,9 @@  - + - + WinExe diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 59a786a11d..ef6c16f2c4 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -2,10 +2,10 @@ - + - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 5847079161..2cc07dd9ed 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -4,9 +4,9 @@ osu.Game.Tournament.Tests.TournamentTestRunner - + - + WinExe diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index 8df2d3fc2d..3fad032531 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -35,8 +35,8 @@ namespace osu.Game.Online.API for (int i = 0; i < itemCount; i++) { - output[reader.ReadString()] = - PrimitiveObjectFormatter.Instance.Deserialize(ref reader, options); + output[reader.ReadString()!] = + PrimitiveObjectFormatter.Instance.Deserialize(ref reader, options)!; } return output; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index c27e30d5bb..9dc24eff69 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -635,7 +635,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private T clone(T incoming) { - byte[]? serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS); + byte[] serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS); return MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS); } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d109345518..b247ea80f6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,13 +21,13 @@ - + - - - - - + + + + + @@ -35,13 +35,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + - + From be026f7ff16b95f85d8a47023506f00572fbf763 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 17 Sep 2023 01:27:43 +0900 Subject: [PATCH 39/71] Bump realm once more --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b247ea80f6..d88b568244 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + 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 40/71] 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 5c2413c06b5a79658cd245871654c7814c558e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 11:30:14 +0200 Subject: [PATCH 41/71] Implement nano beatmap card --- .../Visual/Beatmaps/TestSceneBeatmapCard.cs | 6 + .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 3 + .../Drawables/Cards/BeatmapCardNano.cs | 166 ++++++++++++++++++ .../Drawables/Cards/BeatmapCardSize.cs | 1 + .../BeatmapListingCardSizeTabControl.cs | 4 + 5 files changed, 180 insertions(+) create mode 100644 osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index d4018be7fc..fed26d8acb 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -260,6 +260,12 @@ namespace osu.Game.Tests.Visual.Beatmaps AddStep($"set {scheme} scheme", () => Child = createContent(scheme, creationFunc)); } + [Test] + public void TestNano() + { + createTestCase(beatmapSetInfo => new BeatmapCardNano(beatmapSetInfo)); + } + [Test] public void TestNormal() { diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 94b2956b4e..a16f6d5689 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -89,6 +89,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards { switch (size) { + case BeatmapCardSize.Nano: + return new BeatmapCardNano(beatmapSet); + case BeatmapCardSize.Normal: return new BeatmapCardNormal(beatmapSet, allowExpansion); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs new file mode 100644 index 0000000000..ba2142d28f --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -0,0 +1,166 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +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 BeatmapCardNano : BeatmapCard + { + protected override Drawable IdleContent => idleBottomContent; + protected override Drawable DownloadInProgressContent => downloadProgressBar; + + 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 FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public BeatmapCardNano(APIBeatmapSet beatmapSet) + : base(beatmapSet, false) + { + content = new BeatmapCardContent(height); + } + + [BackgroundDependencyLoader] + private void load() + { + Width = width; + Height = height; + + Child = content.With(c => + { + c.MainContent = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + thumbnail = new BeatmapCardThumbnail(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, + FavouriteState = { BindTarget = FavouriteState }, + ButtonsCollapsedWidth = CORNER_RADIUS, + ButtonsExpandedWidth = 30, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TruncatingSpriteText + { + Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), + Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + new TruncatingSpriteText + { + Text = createArtistText(), + Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + } + }, + new Container + { + Name = @"Bottom content", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + idleBottomContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 3), + AlwaysPresent = true, + Children = new Drawable[] + { + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 2 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(BeatmapSet.Author); + }), + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 6, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = DownloadTracker.State }, + Progress = { BindTarget = DownloadTracker.Progress } + } + } + } + } + } + } + }; + c.ExpandedContent = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, + Child = new BeatmapCardDifficultyList(BeatmapSet) + }; + c.Expanded.BindTarget = Expanded; + }); + } + + private LocalisableString createArtistText() + { + var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist); + return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); + } + + protected override void UpdateState() + { + base.UpdateState(); + + bool showDetails = IsHovered; + + buttonContainer.ShowDetails.Value = showDetails; + thumbnail.Dimmed.Value = showDetails; + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs index 098265506d..0b5acc4a05 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs @@ -8,6 +8,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards /// public enum BeatmapCardSize { + Nano, Normal, Extra } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs index feb0c27ee7..9cd0031e3d 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingCardSizeTabControl.cs @@ -22,8 +22,12 @@ namespace osu.Game.Overlays.BeatmapListing public BeatmapListingCardSizeTabControl() { AutoSizeAxes = Axes.Both; + + Items = new[] { BeatmapCardSize.Normal, BeatmapCardSize.Extra }; } + protected override bool AddEnumEntriesAutomatically => false; + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer { AutoSizeAxes = Axes.Both, From e57d7d1205c2e6914b352ddc451167138d4c3dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Sep 2023 11:50:36 +0200 Subject: [PATCH 42/71] Fix `MemoryStreamArchiveReader.GetStream()` failing in some cases `MemoryStreamArchiveReader` introduced in 0657b551964986fc5504a202eaa5e699d1c72f00 would previously use `MemoryStream.GetBuffer()` to retrieve the underlying byte buffer with stream data. However, this is not generally the method you would want, for two reasons: 1. It can fail if the stream wasn't created in the way that supports it. 2. As per https://learn.microsoft.com/en-us/dotnet/api/system.io.memorystream.getbuffer?view=net-7.0#system-io-memorystream-getbuffer, it will return the _raw_ contents of the buffer, including potentially unused bytes. To fix, use `MemoryStream.ToArray()` instead, which avoids both pitfalls. Notably, `ToArray()` always returns the full contents of the buffer, regardless of `Position`, as documented in: https://learn.microsoft.com/en-us/dotnet/api/system.io.memorystream.toarray?view=net-7.0#system-io-memorystream-toarray --- osu.Game/IO/Archives/MemoryStreamArchiveReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs b/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs index 37ce1e508e..d8e1199e93 100644 --- a/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs +++ b/osu.Game/IO/Archives/MemoryStreamArchiveReader.cs @@ -19,7 +19,7 @@ namespace osu.Game.IO.Archives this.stream = stream; } - public override Stream GetStream(string name) => new MemoryStream(stream.GetBuffer(), 0, (int)stream.Length); + public override Stream GetStream(string name) => new MemoryStream(stream.ToArray(), 0, (int)stream.Length); public override void Dispose() { 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 43/71] 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 44/71] 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 45/71] 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 46/71] 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 47/71] 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 48/71] 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 49/71] 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 50/71] 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 51/71] 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 8e992de763c53230ef37b03a2c9b3807d81ff05d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Sep 2023 05:09:01 +0300 Subject: [PATCH 52/71] Fix crash when loading player instance without exiting previous instance --- .../Visual/Gameplay/TestSceneAllRulesetPlayers.cs | 5 +++++ osu.Game/Tests/Visual/PlayerTestScene.cs | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs index e86302bbd1..63fc4e47f9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs @@ -67,6 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay private Player loadPlayerFor(RulesetInfo rulesetInfo) { + // if a player screen is present already, we must exit that before loading another one, + // otherwise it'll crash on SpectatorClient.BeginPlaying being called while client is in "playing" state already. + if (Stack.CurrentScreen is Player) + Stack.Exit(); + Ruleset.Value = rulesetInfo; var ruleset = rulesetInfo.CreateInstance(); diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 0392e3ae52..ee184c1f35 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { @@ -79,6 +80,11 @@ namespace osu.Game.Tests.Visual protected void LoadPlayer(Mod[] mods) { + // if a player screen is present already, we must exit that before loading another one, + // otherwise it'll crash on SpectatorClient.BeginPlaying being called while client is in "playing" state already. + if (Stack.CurrentScreen is Player) + Stack.Exit(); + var ruleset = CreatePlayerRuleset(); Ruleset.Value = ruleset.RulesetInfo; From 0360646e9b8858ef52ab4ad5599a32dbdc3ddbb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 14:38:53 +0900 Subject: [PATCH 53/71] Avoid fast fade out if slider head was not hit --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 09d98654c3..1a6a0a9ecc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -317,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables switch (state) { case ArmedState.Hit: - if (SliderBody?.SnakingOut.Value == true) + if (HeadCircle.IsHit && SliderBody?.SnakingOut.Value == true) Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear. break; } From 4504c9fc43f6783d07041275a40d3fdff29d8f02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 14:42:07 +0900 Subject: [PATCH 54/71] Update tests in line with new slider snaking behaviour --- .../TestSceneSliderSnaking.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 630049f408..aef7dcaa59 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -135,9 +135,9 @@ namespace osu.Game.Rulesets.Osu.Tests } [Test] - public void TestRepeatArrowDoesNotMoveWhenHit() + public void TestRepeatArrowDoesNotMove([Values] bool useAutoplay) { - AddStep("enable autoplay", () => autoplay = true); + AddStep($"set autoplay to {useAutoplay}", () => autoplay = useAutoplay); setSnaking(true); CreateTest(); // repeat might have a chance to update its position depending on where in the frame its hit, @@ -145,15 +145,6 @@ namespace osu.Game.Rulesets.Osu.Tests addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame); } - [Test] - public void TestRepeatArrowMovesWhenNotHit() - { - AddStep("disable autoplay", () => autoplay = false); - setSnaking(true); - CreateTest(); - addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased); - } - private void retrieveSlider(int index) { AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]); From 046e96afcd9ce4c32fcf2a7c1bcaafe9ba811782 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 14:51:03 +0900 Subject: [PATCH 55/71] Apply NRT to slider snaking tests --- .../TestSceneSliderSnaking.cs | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index aef7dcaa59..13166c2b6b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests public partial class TestSceneSliderSnaking : TestSceneOsuPlayer { [Resolved] - private AudioManager audioManager { get; set; } + private AudioManager audioManager { get; set; } = null!; protected override bool Autoplay => autoplay; private bool autoplay; @@ -41,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly BindableBool snakingIn = new BindableBool(); private readonly BindableBool snakingOut = new BindableBool(); - private IBeatmap beatmap; + private IBeatmap beatmap = null!; private const double duration_of_span = 3605; private const double fade_in_modifier = -1200; - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); [BackgroundDependencyLoader] @@ -57,15 +55,8 @@ namespace osu.Game.Rulesets.Osu.Tests config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); } - private Slider slider; - private DrawableSlider drawableSlider; - - [SetUp] - public void Setup() => Schedule(() => - { - slider = null; - drawableSlider = null; - }); + private Slider slider = null!; + private DrawableSlider? drawableSlider; protected override bool HasCustomSteps => true; @@ -150,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]); addSeekStep(() => slider.StartTime); AddUntilStep("retrieve drawable slider", () => - (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); + (drawableSlider = (DrawableSlider?)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); } private void addEnsureSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased); @@ -170,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests private Func timeAtRepeat(Func startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex; private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd; - private List getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; + private List getSliderCurve() => ((PlaySliderBody)drawableSlider!.Body.Drawable).CurrentCurve; private Vector2 getSliderStart() => getSliderCurve().First(); private Vector2 getSliderEnd() => getSliderCurve().Last(); From c0f603eb0e4007cf8a35e8af4e25d9e01441b1ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 15:27:55 +0900 Subject: [PATCH 56/71] Fix typo in comment --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 07dc2bea54..f80f43bb77 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tournament.Screens.MapPool if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2) return; - // if bans have already been placed, beatmap changes result in a selection being made autoamtically + // if bans have already been placed, beatmap changes result in a selection being made automatically if (beatmap.NewValue?.OnlineID > 0) addForBeatmap(beatmap.NewValue.OnlineID); } 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 57/71] 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 58/71] 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 59/71] 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 62f97a8d83a5baa59da76cdcea179b6be500360a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 10:27:24 +0200 Subject: [PATCH 60/71] Adjust beatmap card thumbnail dim state to match web better --- .../Drawables/Cards/BeatmapCardThumbnail.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index ad91615031..5a26a988fb 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -3,15 +3,15 @@ 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.Game.Beatmaps.Drawables.Cards.Buttons; -using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osuTK; -using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables.Cards { @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards set => foreground.Padding = value; } - private readonly UpdateableOnlineBeatmapSetCover cover; + private readonly Box background; private readonly Container foreground; private readonly PlayButton playButton; private readonly CircularProgress progress; @@ -33,15 +33,22 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Container Content => content; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo) { InternalChildren = new Drawable[] { - cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) + new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both, OnlineInfo = beatmapSetInfo }, + background = new Box + { + RelativeSizeAxes = Axes.Both + }, foreground = new Container { RelativeSizeAxes = Axes.Both, @@ -68,7 +75,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { progress.Colour = colourProvider.Highlight1; } @@ -89,7 +96,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards bool shouldDim = Dimmed.Value || playButton.Playing.Value; playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - cover.FadeColour(shouldDim ? OsuColour.Gray(0.2f) : Color4.White, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.FadeColour(colourProvider.Background6.Opacity(shouldDim ? 0.8f : 0f), BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } } } From 0593c76c57436757e7da3ada6556c683396fdbfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 17:34:24 +0900 Subject: [PATCH 61/71] 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 62/71] 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 63/71] 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 05e05f8160a73dd9f270147874ba0632897d2670 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Sep 2023 18:02:08 +0900 Subject: [PATCH 64/71] Increase transition speed slightly --- osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index a16f6d5689..25e42bcbf7 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu { - public const float TRANSITION_DURATION = 400; + public const float TRANSITION_DURATION = 340; public const float CORNER_RADIUS = 10; protected const float WIDTH = 430; 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 65/71] 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() From 320a9fc17169c81b082c42d16fb750b9fc7e6026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 13:47:46 +0200 Subject: [PATCH 66/71] Replace test with better test --- .../Formats/LegacyBeatmapDecoderTest.cs | 29 +++++++++++-------- osu.Game.Tests/Resources/invalid-bank.osu | 18 ++++++++---- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 6fe9c902bb..1ba63f4037 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -622,7 +622,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } [Test] - public void TestInvalidBankDefaultsToNone() + public void TestInvalidBankDefaultsToNormal() { var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; @@ -631,20 +631,25 @@ namespace osu.Game.Tests.Beatmaps.Formats { var hitObjects = decoder.Decode(stream).HitObjects; - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[0].Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[0].Samples[1].Bank); + assertObjectHasBanks(hitObjects[0], HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[1], HitSampleInfo.BANK_NORMAL); + assertObjectHasBanks(hitObjects[2], HitSampleInfo.BANK_SOFT); + assertObjectHasBanks(hitObjects[3], HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[4], HitSampleInfo.BANK_NORMAL); - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[1].Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[1].Samples[1].Bank); + assertObjectHasBanks(hitObjects[5], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[6], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL); + assertObjectHasBanks(hitObjects[7], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_SOFT); + assertObjectHasBanks(hitObjects[8], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM); + assertObjectHasBanks(hitObjects[9], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL); + } - Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[2].Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[2].Samples[1].Bank); + void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null) + { + Assert.AreEqual(normalBank, hitObject.Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[3].Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_SOFT, hitObjects[3].Samples[1].Bank); - - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[4].Samples[0].Bank); - Assert.AreEqual(HitSampleInfo.BANK_NORMAL, hitObjects[4].Samples[1].Bank); + if (additionsBank != null) + Assert.AreEqual(additionsBank, hitObject.Samples[1].Bank); } } diff --git a/osu.Game.Tests/Resources/invalid-bank.osu b/osu.Game.Tests/Resources/invalid-bank.osu index fb54a61fd3..8c554cc17f 100644 --- a/osu.Game.Tests/Resources/invalid-bank.osu +++ b/osu.Game.Tests/Resources/invalid-bank.osu @@ -3,9 +3,17 @@ osu file format v14 [General] SampleSet: Normal +[TimingPoints] +0,500,4,3,0,100,1,0 + [HitObjects] -256,192,1000,1,8,0:0:0:0: -256,192,2000,1,8,1:2:0:0: -256,192,3000,1,8,2:62:0:0: -256,192,4000,1,8,41:2:0:0: -256,192,5000,1,8,41:62:0:0: +256,192,1000,5,0,0:0:0:0: +256,192,2000,1,0,1:0:0:0: +256,192,3000,1,0,2:0:0:0: +256,192,4000,1,0,3:0:0:0: +256,192,5000,1,0,42:0:0:0: +256,192,6000,5,4,0:0:0:0: +256,192,7000,1,4,0:1:0:0: +256,192,8000,1,4,0:2:0:0: +256,192,9000,1,4,0:3:0:0: +256,192,10000,1,4,0:42:0:0: From c4a0ca326ed5e02a9219dee579cb9a01c554960b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 13:53:49 +0200 Subject: [PATCH 67/71] Replace sample bank fix with more correct fix stable does not treat unknown enum members as `None` / `Auto`, it treats them as `Normal`: switch (sampleSet) { case SampleSet.Normal: default: sample = 0; break; case SampleSet.None: case SampleSet.Soft: sample = 1; break; case SampleSet.Drum: sample = 2; break; } (from https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Audio/AudioEngine.cs#L1158-L1171). --- .../Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 339e9bb5bc..d20f2d31bb 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -190,13 +190,18 @@ namespace osu.Game.Rulesets.Objects.Legacy string[] split = str.Split(':'); var bank = (LegacySampleBank)Parsing.ParseInt(split[0]); + if (!Enum.IsDefined(bank)) + bank = LegacySampleBank.Normal; + var addBank = (LegacySampleBank)Parsing.ParseInt(split[1]); + if (!Enum.IsDefined(addBank)) + addBank = LegacySampleBank.Normal; string stringBank = bank.ToString().ToLowerInvariant(); - if (stringBank == @"none" || !Enum.IsDefined(bank)) + if (stringBank == @"none") stringBank = null; string stringAddBank = addBank.ToString().ToLowerInvariant(); - if (stringAddBank == @"none" || !Enum.IsDefined(addBank)) + if (stringAddBank == @"none") stringAddBank = null; bankInfo.BankForNormal = stringBank; From ba518e1da8442d8aaf17a235d227a394f077689e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 20:11:16 +0200 Subject: [PATCH 68/71] Fix `StoryboardResourceLookupStore` dying on failure to unmap path Before the introduction of `StoryboardResourceLookupStore`, missing files would softly fail by use of null fallbacks. After the aforementioned class was added, however, the fallbacks would not work anymore if for whatever reason `GetStoragePathFromStoryboardPath()` failed to unmap the storyboard asset name to a storage path. --- .../Drawables/DrawableStoryboard.cs | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 6931cea81e..c2a58d46ef 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -142,14 +142,32 @@ namespace osu.Game.Storyboards.Drawables public void Dispose() => realmFileStore.Dispose(); - public byte[] Get(string name) => - realmFileStore.Get(storyboard.GetStoragePathFromStoryboardPath(name)); + public byte[] Get(string name) + { + string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name); - public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => - realmFileStore.GetAsync(storyboard.GetStoragePathFromStoryboardPath(name), cancellationToken); + return string.IsNullOrEmpty(storagePath) + ? null! + : realmFileStore.Get(storagePath); + } - public Stream GetStream(string name) => - realmFileStore.GetStream(storyboard.GetStoragePathFromStoryboardPath(name)); + public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name); + + return string.IsNullOrEmpty(storagePath) + ? Task.FromResult(null!) + : realmFileStore.GetAsync(storagePath, cancellationToken); + } + + public Stream? GetStream(string name) + { + string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name); + + return string.IsNullOrEmpty(storagePath) + ? null + : realmFileStore.GetStream(storagePath); + } public IEnumerable GetAvailableResources() => realmFileStore.GetAvailableResources(); From 641e651bf282aeeb11050756e63e3a98cc136bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 20:18:33 +0200 Subject: [PATCH 69/71] Fix `DrawableStoryboardVideo` attempting to unmap path once too much The `StoryboardResourceLookupStore` cached at storyboard level is supposed to already be handling that; no need for local logic anymore. --- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index eec2cd6a60..9a5db4bb39 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -29,12 +29,7 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader(true)] private void load(IBindable beatmap, TextureStore textureStore) { - string? path = beatmap.Value.BeatmapSetInfo?.GetPathForFile(Video.Path); - - if (path == null) - return; - - var stream = textureStore.GetStream(path); + var stream = textureStore.GetStream(Video.Path); if (stream == null) return; From 333b839e0d7b859aa677a6a27163cb894c7d5563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Sep 2023 21:37:44 +0200 Subject: [PATCH 70/71] Fix broken automatic beatmap download setting migration --- osu.Game/Configuration/OsuConfigManager.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index b5253d3500..db71ff4e84 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -64,6 +64,12 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Token, string.Empty); +#pragma warning disable CS0618 // Type or member is obsolete + // this default set MUST remain despite the setting being deprecated, because `SetDefault()` calls are implicitly used to declare the type returned for the lookup. + // if this is removed, the setting will be interpreted as a string, and `Migrate()` will fail due to cast failure. + // can be removed 20240618 + SetDefault(OsuSetting.AutomaticallyDownloadWhenSpectating, false); +#pragma warning restore CS0618 // Type or member is obsolete SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false); SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled => @@ -218,7 +224,7 @@ namespace osu.Game.Configuration if (combined < 20230918) { #pragma warning disable CS0618 // Type or member is obsolete - SetValue(OsuSetting.AutomaticallyDownloadMissingBeatmaps, Get(OsuSetting.AutomaticallyDownloadWhenSpectating)); // can be removed 20240618 + SetValue(OsuSetting.AutomaticallyDownloadMissingBeatmaps, Get(OsuSetting.AutomaticallyDownloadWhenSpectating)); // can be removed 20240618 #pragma warning restore CS0618 // Type or member is obsolete } } From 71ac5cfc792a2aec35720e08f6d02d87e69995be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Sep 2023 14:14:37 +0900 Subject: [PATCH 71/71] Don't bother binding to friends changes for score display purposes --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 502303e80c..7471955493 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -13,7 +13,6 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Scoring; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -110,7 +109,7 @@ namespace osu.Game.Screens.Play.HUD private IBindable scoreDisplayMode = null!; - private readonly IBindableList apiFriends = new BindableList(); + private bool isFriend; /// /// Creates a new . @@ -317,8 +316,7 @@ namespace osu.Game.Screens.Play.HUD HasQuit.BindValueChanged(_ => updateState()); - apiFriends.BindTo(api.Friends); - apiFriends.BindCollectionChanged((_, _) => updateState()); + isFriend = User != null && api.Friends.Any(u => User.OnlineID == u.Id); } protected override void LoadComplete() @@ -397,7 +395,7 @@ namespace osu.Game.Screens.Play.HUD panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966"); textColour = TextColour ?? Color4Extensions.FromHex("2e576b"); } - else if (apiFriends.Any(f => User?.Equals(f) == true)) + else if (isFriend) { panelColour = BackgroundColour ?? Color4Extensions.FromHex("ff549a"); textColour = TextColour ?? Color4.White;