1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 19:27:24 +08:00

Merge pull request #24450 from cdwcgt/missing-beatmap

Fetch missing beatmap when importing replay
This commit is contained in:
Dean Herbert 2023-09-19 18:40:02 +09:00 committed by GitHub
commit 067c487b21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 301 additions and 26 deletions

View File

@ -0,0 +1,97 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.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,
BeatmapSet = new APIBeatmapSet
{
Title = "FREEDOM Dive",
Artist = "xi",
Covers = new BeatmapSetOnlineCovers
{
Card = "https://assets.ppy.sh/beatmaps/173612/covers/card@2x.jpg"
},
OnlineID = 173612
}
};
setupBeatmapResponse(beatmap);
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 notification show", () => Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
}
[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 notification not show", () => !Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
}
private void setupBeatmapResponse(APIBeatmap b)
=> AddStep("setup response", () =>
{
dummyAPI.HandleRequest = request =>
{
if (request is GetBeatmapRequest getBeatmapRequest)
{
getBeatmapRequest.TriggerSuccess(b);
return true;
}
return false;
};
});
private void setupFailedResponse()
=> AddStep("setup failed response", () =>
{
dummyAPI.HandleRequest = request =>
{
request.TriggerFailure(new WebException());
return true;
};
});
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Database;
using osu.Game.Overlays;
using osu.Game.Tests.Scores.IO;
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 ImportScoreTest.TestArchiveReader(), "deadbeef")
};
}
}
}

View File

@ -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
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r => r.All<BeatmapInfo>().FirstOrDefault(query)?.Detach());
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r => r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.

View File

@ -20,14 +20,24 @@ 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 FillFlowContainer idleBottomContent = null!;
@ -52,21 +62,15 @@ 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)
{
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,
ButtonsCollapsedWidth = 5,
ButtonsExpandedWidth = 30,
Children = new Drawable[]
{
@ -160,7 +164,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
bool showDetails = IsHovered;
buttonContainer.ShowDetails.Value = showDetails;
thumbnail.Dimmed.Value = showDetails;
}
}
}

View File

@ -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>(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,
}
}

View File

@ -0,0 +1,103 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.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;
namespace osu.Game.Database
{
public partial class MissingBeatmapNotification : SimpleNotification
{
[Resolved]
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
[Resolved]
private ScoreManager scoreManager { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
private readonly ArchiveReader scoreArchive;
private readonly APIBeatmapSet beatmapSetInfo;
private readonly string beatmapHash;
private Bindable<bool> autoDownloadConfig = null!;
private Bindable<bool> noVideoSetting = null!;
private BeatmapCardNano card = null!;
private IDisposable? realmSubscription;
public MissingBeatmapNotification(APIBeatmap beatmap, ArchiveReader scoreArchive, string beatmapHash)
{
beatmapSetInfo = beatmap.BeatmapSet!;
this.beatmapHash = beatmapHash;
this.scoreArchive = scoreArchive;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
realmSubscription = realm.RegisterForNotifications(
realm => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
autoDownloadConfig = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps);
noVideoSetting = config.GetBindable<bool>(OsuSetting.PreferNoVideo);
Content.Add(card = new BeatmapCardNano(beatmapSetInfo));
}
protected override void LoadComplete()
{
base.LoadComplete();
if (autoDownloadConfig.Value)
{
Text = NotificationsStrings.DownloadingBeatmapForReplay;
beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value);
}
else
{
bool missingSetMatchesExistingOnlineId = realm.Run(r => r.All<BeatmapSetInfo>().Any(s => !s.DeletePending && s.OnlineID == beatmapSetInfo.OnlineID));
Text = missingSetMatchesExistingOnlineId ? NotificationsStrings.MismatchingBeatmapForReplay : NotificationsStrings.MissingBeatmapForReplay;
}
}
protected override void Update()
{
base.Update();
card.Width = Content.DrawWidth;
}
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{
if (changes?.InsertedIndices == null) return;
if (sender.Any(s => s.Beatmaps.Any(b => b.MD5Hash == beatmapHash)))
{
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);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
realmSubscription?.Dispose();
}
}
}

View File

@ -93,6 +93,21 @@ Please try changing your audio device to a working setting.");
/// </summary>
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);
/// <summary>
/// "You do not have the beatmap for this replay."
/// </summary>
public static LocalisableString MissingBeatmapForReplay => new TranslatableString(getKey(@"missing_beatmap_for_replay"), @"You do not have the beatmap for this replay.");
/// <summary>
/// "Downloading missing beatmap for this replay..."
/// </summary>
public static LocalisableString DownloadingBeatmapForReplay => new TranslatableString(getKey(@"downloading_beatmap_for_replay"), @"Downloading missing beatmap for this replay...");
/// <summary>
/// "Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it."
/// </summary>
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}";
}
}

View File

@ -55,9 +55,9 @@ namespace osu.Game.Localisation
public static LocalisableString PreferNoVideo => new TranslatableString(getKey(@"prefer_no_video"), @"Prefer downloads without video");
/// <summary>
/// "Automatically download beatmaps when spectating"
/// "Automatically download missing beatmaps"
/// </summary>
public static LocalisableString AutomaticallyDownloadWhenSpectating => new TranslatableString(getKey(@"automatically_download_when_spectating"), @"Automatically download beatmaps when spectating");
public static LocalisableString AutomaticallyDownloadMissingBeatmaps => new TranslatableString(getKey(@"automatically_download_missing_beatmaps"), @"Automatically download missing beatmaps");
/// <summary>
/// "Show explicit content in search results"

View File

@ -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<Drawable> 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)
},
}
},

View File

@ -31,9 +31,9 @@ namespace osu.Game.Overlays.Settings.Sections.Online
},
new SettingsCheckbox
{
LabelText = OnlineSettingsStrings.AutomaticallyDownloadWhenSpectating,
Keywords = new[] { "spectator" },
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating),
LabelText = OnlineSettingsStrings.AutomaticallyDownloadMissingBeatmaps,
Keywords = new[] { "spectator", "replay" },
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps),
},
new SettingsCheckbox
{

View File

@ -54,7 +54,12 @@ namespace osu.Game.Scoring
}
catch (LegacyScoreDecoder.BeatmapNotFoundException e)
{
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);
// 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;
}
}

View File

@ -143,7 +143,7 @@ namespace osu.Game.Screens.Play
automaticDownload = new SettingsCheckbox
{
LabelText = "Automatically download beatmaps",
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating),
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},

View File

@ -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) =>