mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 04:52:57 +08:00
Merge pull request #24450 from cdwcgt/missing-beatmap
Fetch missing beatmap when importing replay
This commit is contained in:
commit
067c487b21
@ -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;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@ -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")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
103
osu.Game/Database/MissingBeatmapNotification.cs
Normal file
103
osu.Game/Database/MissingBeatmapNotification.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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) =>
|
||||
|
Loading…
Reference in New Issue
Block a user