1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 14:17:26 +08:00

fetch missing beatmap when import score with missing beatmap

This commit is contained in:
cdwcgt 2023-08-02 15:01:58 +09:00
parent dc9c68b642
commit eef63b41da
No known key found for this signature in database
GPG Key ID: 144396D01095C3A2
6 changed files with 349 additions and 2 deletions

View File

@ -0,0 +1,96 @@
// 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.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;
};
});
}
}

View File

@ -854,6 +854,8 @@ namespace osu.Game
MultiplayerClient.PostNotification = n => Notifications.Post(n); MultiplayerClient.PostNotification = n => Notifications.Post(n);
ScoreManager.Performer = this;
// make config aware of how to lookup skins for on-screen display purposes. // 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. // 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"; LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown";

View File

@ -7,6 +7,7 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
@ -61,7 +62,7 @@ namespace osu.Game.Scoring.Legacy
workingBeatmap = GetBeatmap(beatmapHash); workingBeatmap = GetBeatmap(beatmapHash);
if (workingBeatmap is DummyWorkingBeatmap) if (workingBeatmap is DummyWorkingBeatmap)
throw new BeatmapNotFoundException(beatmapHash); throw new BeatmapNotFoundException(beatmapHash, stream);
scoreInfo.User = new APIUser { Username = sr.ReadString() }; scoreInfo.User = new APIUser { Username = sr.ReadString() };
@ -349,9 +350,19 @@ namespace osu.Game.Scoring.Legacy
{ {
public string Hash { get; } public string Hash { get; }
public BeatmapNotFoundException(string hash) [CanBeNull]
public MemoryStream ScoreStream { get; }
public BeatmapNotFoundException(string hash, [CanBeNull] Stream scoreStream)
{ {
Hash = hash; Hash = hash;
if (scoreStream != null)
{
ScoreStream = new MemoryStream();
scoreStream.Position = 0;
scoreStream.CopyTo(ScoreStream);
}
} }
} }
} }

View File

@ -9,6 +9,7 @@ using System.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO.Archives; 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.Online.API.Requests.Responses;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Import;
using Realms; using Realms;
namespace osu.Game.Scoring namespace osu.Game.Scoring
@ -27,6 +30,8 @@ namespace osu.Game.Scoring
{ {
public override IEnumerable<string> HandledExtensions => new[] { ".osr" }; public override IEnumerable<string> HandledExtensions => new[] { ".osr" };
public IPerformFromScreenRunner? Performer { get; set; }
protected override string[] HashableFileTypes => new[] { ".osr" }; protected override string[] HashableFileTypes => new[] { ".osr" };
private readonly RulesetStore rulesets; private readonly RulesetStore rulesets;
@ -54,12 +59,28 @@ namespace osu.Game.Scoring
} }
catch (LegacyScoreDecoder.BeatmapNotFoundException e) 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); Logger.Log($@"Score '{name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database);
return null; 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); 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) protected override void Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)

View File

@ -21,6 +21,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Scoring.Legacy; using osu.Game.Scoring.Legacy;
using osu.Game.Screens;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
@ -30,6 +31,12 @@ namespace osu.Game.Scoring
private readonly ScoreImporter scoreImporter; private readonly ScoreImporter scoreImporter;
private readonly LegacyScoreExporter scoreExporter; private readonly LegacyScoreExporter scoreExporter;
[CanBeNull]
public IPerformFromScreenRunner Performer
{
set => scoreImporter.Performer = value;
}
public override bool PauseImports public override bool PauseImports
{ {
get => base.PauseImports; get => base.PauseImports;

View File

@ -0,0 +1,210 @@
// 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.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<bool>(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<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
}
private void checkForAutomaticDownload(APIBeatmapSet beatmap)
{
if (!automaticDownload.Current.Value)
return;
beatmapDownloader.Download(beatmap);
}
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> 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<IEnumerable<Live<ScoreInfo>>>().FirstOrDefault()?.PerformRead(score =>
{
Guid scoreid = score.ID;
Scheduler.Add(() =>
{
replayDownloadButton.Score.Value = realm.Realm.Find<ScoreInfo>(scoreid) ?? new ScoreInfo();
});
});
});
notificationOverlay?.Post(progressNotification);
realmSubscription?.Dispose();
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
realmSubscription?.Dispose();
}
}
}