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

Merge pull request #8260 from smoogipoo/add-workingbeatmap-timeout

Add beatmap loading timeout to prevent runaway loading scenarios
This commit is contained in:
Dean Herbert 2020-03-17 11:17:35 +09:00 committed by GitHub
commit 5e3668b2ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 109 additions and 53 deletions

View File

@ -1,6 +1,7 @@
// 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 osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
@ -60,8 +61,9 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="ruleset">The <see cref="RulesetInfo"/> to create a playable <see cref="IBeatmap"/> for.</param>
/// <param name="mods">The <see cref="Mod"/>s to apply to the <see cref="IBeatmap"/>.</param>
/// <param name="timeout">The maximum length in milliseconds to wait for load to complete. Defaults to 10,000ms.</param>
/// <returns>The converted <see cref="IBeatmap"/>.</returns>
/// <exception cref="BeatmapInvalidForRulesetException">If <see cref="Beatmap"/> could not be converted to <paramref name="ruleset"/>.</exception>
IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null);
IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null);
}
}

View File

@ -83,55 +83,81 @@ namespace osu.Game.Beatmaps
/// <returns>The applicable <see cref="IBeatmapConverter"/>.</returns>
protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap);
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null)
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null)
{
mods ??= Array.Empty<Mod>();
var rulesetInstance = ruleset.CreateInstance();
IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance);
// Check if the beatmap can be converted
if (Beatmap.HitObjects.Count > 0 && !converter.CanConvert())
throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter}).");
// Apply conversion mods
foreach (var mod in mods.OfType<IApplicableToBeatmapConverter>())
mod.ApplyToBeatmapConverter(converter);
// Convert
IBeatmap converted = converter.Convert();
// Apply difficulty mods
if (mods.Any(m => m is IApplicableToDifficulty))
using (var cancellationSource = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10)))
{
converted.BeatmapInfo = converted.BeatmapInfo.Clone();
converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone();
mods ??= Array.Empty<Mod>();
foreach (var mod in mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty);
}
var rulesetInstance = ruleset.CreateInstance();
IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted);
IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance);
processor?.PreProcess();
// Check if the beatmap can be converted
if (Beatmap.HitObjects.Count > 0 && !converter.CanConvert())
throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter}).");
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
foreach (var obj in converted.HitObjects)
obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty);
// Apply conversion mods
foreach (var mod in mods.OfType<IApplicableToBeatmapConverter>())
{
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
foreach (var mod in mods.OfType<IApplicableToHitObject>())
{
mod.ApplyToBeatmapConverter(converter);
}
// Convert
IBeatmap converted = converter.Convert();
// Apply difficulty mods
if (mods.Any(m => m is IApplicableToDifficulty))
{
converted.BeatmapInfo = converted.BeatmapInfo.Clone();
converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone();
foreach (var mod in mods.OfType<IApplicableToDifficulty>())
{
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty);
}
}
IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted);
processor?.PreProcess();
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
foreach (var obj in converted.HitObjects)
mod.ApplyToHitObject(obj);
{
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty);
}
foreach (var mod in mods.OfType<IApplicableToHitObject>())
{
foreach (var obj in converted.HitObjects)
{
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
mod.ApplyToHitObject(obj);
}
}
processor?.PostProcess();
foreach (var mod in mods.OfType<IApplicableToBeatmap>())
{
cancellationSource.Token.ThrowIfCancellationRequested();
mod.ApplyToBeatmap(converted);
}
return converted;
}
processor?.PostProcess();
foreach (var mod in mods.OfType<IApplicableToBeatmap>())
mod.ApplyToBeatmap(converted);
return converted;
}
private CancellationTokenSource loadCancellation = new CancellationTokenSource();
@ -297,5 +323,13 @@ namespace osu.Game.Beatmaps
private void recreate() => lazy = new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication);
}
private class BeatmapLoadTimeoutException : TimeoutException
{
public BeatmapLoadTimeoutException(BeatmapInfo beatmapInfo)
: base($"Timed out while loading beatmap ({beatmapInfo}).")
{
}
}
}
}

View File

@ -23,6 +23,7 @@ using osuTK.Input;
using System.Collections.Generic;
using osu.Framework;
using osu.Framework.Input.Bindings;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor;
using osu.Game.Input.Bindings;
@ -86,7 +87,18 @@ namespace osu.Game.Screens.Edit
// todo: remove caching of this and consume via editorBeatmap?
dependencies.Cache(beatDivisor);
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
try
{
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
}
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap successfully!");
// couldn't load, hard abort!
this.Exit();
return;
}
AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap));
dependencies.CacheAs(editorBeatmap);

View File

@ -23,6 +23,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@ -311,20 +312,27 @@ namespace osu.Game.Screens.Select
Content = getBPMRange(b),
}));
IBeatmap playableBeatmap;
try
{
// Try to get the beatmap with the user's ruleset
playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty<Mod>());
}
catch (BeatmapInvalidForRulesetException)
{
// Can't be converted to the user's ruleset, so use the beatmap's own ruleset
playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>());
}
IBeatmap playableBeatmap;
labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)));
try
{
// Try to get the beatmap with the user's ruleset
playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty<Mod>());
}
catch (BeatmapInvalidForRulesetException)
{
// Can't be converted to the user's ruleset, so use the beatmap's own ruleset
playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>());
}
labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)));
}
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap successfully!");
}
}
return labels.ToArray();