1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-05 16:02:56 +08:00

Merge pull request #12492 from Naxesss/bg-audio-quality-checks

Add background and audio checks
This commit is contained in:
Dean Herbert 2021-04-21 00:10:32 +09:00 committed by GitHub
commit 26430e3cb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 628 additions and 133 deletions

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Checks; using osu.Game.Rulesets.Osu.Edit.Checks;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
@ -30,25 +31,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
[Test] [Test]
public void TestCircleInCenter() public void TestCircleInCenter()
{ {
var beatmap = new Beatmap<HitObject> assertOk(new Beatmap<HitObject>
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
new HitCircle new HitCircle
{ {
StartTime = 3000, StartTime = 3000,
Position = playfield_centre // Playfield is 640 x 480. Position = playfield_centre
} }
} }
}; });
Assert.That(check.Run(beatmap), Is.Empty);
} }
[Test] [Test]
public void TestCircleNearEdge() public void TestCircleNearEdge()
{ {
var beatmap = new Beatmap<HitObject> assertOk(new Beatmap<HitObject>
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -58,15 +57,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = new Vector2(5, 5) Position = new Vector2(5, 5)
} }
} }
}; });
Assert.That(check.Run(beatmap), Is.Empty);
} }
[Test] [Test]
public void TestCircleNearEdgeStackedOffscreen() public void TestCircleNearEdgeStackedOffscreen()
{ {
var beatmap = new Beatmap<HitObject> assertOffscreenCircle(new Beatmap<HitObject>
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -77,15 +74,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
StackHeight = 5 StackHeight = 5
} }
} }
}; });
assertOffscreenCircle(beatmap);
} }
[Test] [Test]
public void TestCircleOffscreen() public void TestCircleOffscreen()
{ {
var beatmap = new Beatmap<HitObject> assertOffscreenCircle(new Beatmap<HitObject>
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -95,15 +90,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = new Vector2(0, 0) Position = new Vector2(0, 0)
} }
} }
}; });
assertOffscreenCircle(beatmap);
} }
[Test] [Test]
public void TestSliderInCenter() public void TestSliderInCenter()
{ {
var beatmap = new Beatmap<HitObject> assertOk(new Beatmap<HitObject>
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -118,15 +111,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
}), }),
} }
} }
}; });
Assert.That(check.Run(beatmap), Is.Empty);
} }
[Test] [Test]
public void TestSliderNearEdge() public void TestSliderNearEdge()
{ {
var beatmap = new Beatmap<HitObject> assertOk(new Beatmap<HitObject>
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -141,15 +132,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
}), }),
} }
} }
}; });
Assert.That(check.Run(beatmap), Is.Empty);
} }
[Test] [Test]
public void TestSliderNearEdgeStackedOffscreen() public void TestSliderNearEdgeStackedOffscreen()
{ {
var beatmap = new Beatmap<HitObject> assertOffscreenSlider(new Beatmap<HitObject>
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -165,15 +154,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
StackHeight = 5 StackHeight = 5
} }
} }
}; });
assertOffscreenSlider(beatmap);
} }
[Test] [Test]
public void TestSliderOffscreenStart() public void TestSliderOffscreenStart()
{ {
var beatmap = new Beatmap<HitObject> assertOffscreenSlider(new Beatmap<HitObject>
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -188,15 +175,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
}), }),
} }
} }
}; });
assertOffscreenSlider(beatmap);
} }
[Test] [Test]
public void TestSliderOffscreenEnd() public void TestSliderOffscreenEnd()
{ {
var beatmap = new Beatmap<HitObject> assertOffscreenSlider(new Beatmap<HitObject>
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -211,15 +196,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
}), }),
} }
} }
}; });
assertOffscreenSlider(beatmap);
} }
[Test] [Test]
public void TestSliderOffscreenPath() public void TestSliderOffscreenPath()
{ {
var beatmap = new Beatmap<HitObject> assertOffscreenSlider(new Beatmap<HitObject>
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
{ {
@ -236,14 +219,17 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
}), }),
} }
} }
}; });
}
assertOffscreenSlider(beatmap); private void assertOk(IBeatmap beatmap)
{
Assert.That(check.Run(beatmap, new TestWorkingBeatmap(beatmap)), Is.Empty);
} }
private void assertOffscreenCircle(IBeatmap beatmap) private void assertOffscreenCircle(IBeatmap beatmap)
{ {
var issues = check.Run(beatmap).ToList(); var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(1)); Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle); Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle);
@ -251,7 +237,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
private void assertOffscreenSlider(IBeatmap beatmap) private void assertOffscreenSlider(IBeatmap beatmap)
{ {
var issues = check.Run(beatmap).ToList(); var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(1)); Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenSlider); Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenSlider);

View File

@ -31,9 +31,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
new IssueTemplateOffscreenSlider(this) new IssueTemplateOffscreenSlider(this)
}; };
public IEnumerable<Issue> Run(IBeatmap beatmap) public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
{ {
foreach (var hitobject in beatmap.HitObjects) foreach (var hitobject in playableBeatmap.HitObjects)
{ {
switch (hitobject) switch (hitobject)
{ {

View File

@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.Osu.Edit
new CheckOffscreenObjects() new CheckOffscreenObjects()
}; };
public IEnumerable<Issue> Run(IBeatmap beatmap) => checks.SelectMany(check => check.Run(beatmap)); public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)
{
return checks.SelectMany(check => check.Run(playableBeatmap, workingBeatmap));
}
} }
} }

View File

@ -168,6 +168,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override Texture GetBackground() => throw new NotImplementedException(); protected override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
} }
} }
} }

View File

@ -0,0 +1,114 @@
// 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 Moq;
using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckAudioQualityTest
{
private CheckAudioQuality check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckAudioQuality();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" }
}
};
}
[Test]
public void TestMissing()
{
// While this is a problem, it is out of scope for this check and is caught by a different one.
beatmap.Metadata.AudioFile = null;
var mock = new Mock<IWorkingBeatmap>();
mock.SetupGet(w => w.Beatmap).Returns(beatmap);
mock.SetupGet(w => w.Track).Returns((Track)null);
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
}
[Test]
public void TestAcceptable()
{
var mock = getMockWorkingBeatmap(192);
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
}
[Test]
public void TestNullBitrate()
{
var mock = getMockWorkingBeatmap(null);
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateNoBitrate);
}
[Test]
public void TestZeroBitrate()
{
var mock = getMockWorkingBeatmap(0);
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateNoBitrate);
}
[Test]
public void TestTooHighBitrate()
{
var mock = getMockWorkingBeatmap(320);
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
}
[Test]
public void TestTooLowBitrate()
{
var mock = getMockWorkingBeatmap(64);
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooLowBitrate);
}
/// <summary>
/// Returns the mock of the working beatmap with the given audio properties.
/// </summary>
/// <param name="audioBitrate">The bitrate of the audio file the beatmap uses.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate)
{
var mockTrack = new Mock<Track>();
mockTrack.SetupGet(t => t.Bitrate).Returns(audioBitrate);
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap);
mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack.Object);
return mockWorkingBeatmap;
}
}
}

View File

@ -0,0 +1,130 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using Moq;
using NUnit.Framework;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckBackgroundQualityTest
{
private CheckBackgroundQuality check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckBackgroundQuality();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" },
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo
{
Filename = "abc123.jpg",
FileInfo = new FileInfo
{
Hash = "abcdef"
}
}
})
}
}
};
}
[Test]
public void TestMissing()
{
// While this is a problem, it is out of scope for this check and is caught by a different one.
beatmap.Metadata.BackgroundFile = null;
var mock = getMockWorkingBeatmap(null, System.Array.Empty<byte>());
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
}
[Test]
public void TestAcceptable()
{
var mock = getMockWorkingBeatmap(new Texture(1920, 1080));
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
}
[Test]
public void TestTooHighResolution()
{
var mock = getMockWorkingBeatmap(new Texture(3840, 2160));
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooHighResolution);
}
[Test]
public void TestLowResolution()
{
var mock = getMockWorkingBeatmap(new Texture(640, 480));
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateLowResolution);
}
[Test]
public void TestTooLowResolution()
{
var mock = getMockWorkingBeatmap(new Texture(100, 100));
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooLowResolution);
}
[Test]
public void TestTooUncompressed()
{
var mock = getMockWorkingBeatmap(new Texture(1920, 1080), new byte[1024 * 1024 * 3]);
var issues = check.Run(beatmap, mock.Object).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooUncompressed);
}
/// <summary>
/// Returns the mock of the working beatmap with the given background and filesize.
/// </summary>
/// <param name="background">The texture of the background.</param>
/// <param name="fileBytes">The bytes that represent the background file.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(Texture background, [CanBeNull] byte[] fileBytes = null)
{
var stream = new MemoryStream(fileBytes ?? new byte[1024 * 1024]);
var mock = new Mock<IWorkingBeatmap>();
mock.SetupGet(w => w.Beatmap).Returns(beatmap);
mock.SetupGet(w => w.Background).Returns(background);
mock.Setup(w => w.GetStream(It.IsAny<string>())).Returns(stream);
return mock;
}
}
}

View File

@ -5,21 +5,23 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks namespace osu.Game.Tests.Editing.Checks
{ {
[TestFixture] [TestFixture]
public class CheckBackgroundTest public class CheckFilePresenceTest
{ {
private CheckBackground check; private CheckBackgroundPresence check;
private IBeatmap beatmap; private IBeatmap beatmap;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
check = new CheckBackground(); check = new CheckBackgroundPresence();
beatmap = new Beatmap<HitObject> beatmap = new Beatmap<HitObject>
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
@ -29,7 +31,11 @@ namespace osu.Game.Tests.Editing.Checks
{ {
Files = new List<BeatmapSetFileInfo>(new[] Files = new List<BeatmapSetFileInfo>(new[]
{ {
new BeatmapSetFileInfo { Filename = "abc123.jpg" } new BeatmapSetFileInfo
{
Filename = "abc123.jpg",
FileInfo = new FileInfo { Hash = "abcdef" }
}
}) })
} }
} }
@ -39,7 +45,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestBackgroundSetAndInFiles() public void TestBackgroundSetAndInFiles()
{ {
Assert.That(check.Run(beatmap), Is.Empty); Assert.That(check.Run(beatmap, new TestWorkingBeatmap(beatmap)), Is.Empty);
} }
[Test] [Test]
@ -47,10 +53,10 @@ namespace osu.Game.Tests.Editing.Checks
{ {
beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
var issues = check.Run(beatmap).ToList(); var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(1)); Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackground.IssueTemplateDoesNotExist); Assert.That(issues.Single().Template is CheckFilePresence.IssueTemplateDoesNotExist);
} }
[Test] [Test]
@ -58,10 +64,10 @@ namespace osu.Game.Tests.Editing.Checks
{ {
beatmap.Metadata.BackgroundFile = null; beatmap.Metadata.BackgroundFile = null;
var issues = check.Run(beatmap).ToList(); var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
Assert.That(issues, Has.Count.EqualTo(1)); Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackground.IssueTemplateNoneSet); Assert.That(issues.Single().Template is CheckFilePresence.IssueTemplateNoneSet);
} }
} }
} }

View File

@ -52,6 +52,8 @@ namespace osu.Game.Tests
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
public override Stream GetStream(string storagePath) => null;
protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile); protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile);
private string firstAudioFile private string firstAudioFile

View File

@ -526,6 +526,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => beatmap; protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null; protected override Texture GetBackground() => null;
protected override Track GetBeatmapTrack() => null; protected override Track GetBeatmapTrack() => null;
public override Stream GetStream(string storagePath) => null;
} }
} }

View File

@ -3,7 +3,7 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.IO;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps
try try
{ {
using (var stream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapInfo.Path)))) using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream); return Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
} }
catch (Exception e) catch (Exception e)
@ -46,8 +46,6 @@ namespace osu.Game.Beatmaps
} }
} }
private string getPathForFile(string filename) => BeatmapSetInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath;
protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes.
protected override Texture GetBackground() protected override Texture GetBackground()
@ -57,7 +55,7 @@ namespace osu.Game.Beatmaps
try try
{ {
return resources.LargeTextureStore.Get(getPathForFile(Metadata.BackgroundFile)); return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile));
} }
catch (Exception e) catch (Exception e)
{ {
@ -73,7 +71,7 @@ namespace osu.Game.Beatmaps
try try
{ {
return resources.Tracks.Get(getPathForFile(Metadata.AudioFile)); return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
} }
catch (Exception e) catch (Exception e)
{ {
@ -89,7 +87,7 @@ namespace osu.Game.Beatmaps
try try
{ {
var trackData = resources.Files.GetStream(getPathForFile(Metadata.AudioFile)); var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
return trackData == null ? null : new Waveform(trackData); return trackData == null ? null : new Waveform(trackData);
} }
catch (Exception e) catch (Exception e)
@ -105,7 +103,7 @@ namespace osu.Game.Beatmaps
try try
{ {
using (var stream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapInfo.Path)))) using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
{ {
var decoder = Decoder.GetDecoder<Storyboard>(stream); var decoder = Decoder.GetDecoder<Storyboard>(stream);
@ -114,7 +112,7 @@ namespace osu.Game.Beatmaps
storyboard = decoder.Decode(stream); storyboard = decoder.Decode(stream);
else else
{ {
using (var secondaryStream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile))))
storyboard = decoder.Decode(stream, secondaryStream); storyboard = decoder.Decode(stream, secondaryStream);
} }
} }
@ -142,6 +140,8 @@ namespace osu.Game.Beatmaps
return null; return null;
} }
} }
public override Stream GetStream(string storagePath) => resources.Files.GetStream(storagePath);
} }
} }
} }

View File

@ -59,6 +59,13 @@ namespace osu.Game.Beatmaps
public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename; public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
/// <summary>
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// </summary>
/// <param name="filename">The name of the file to get the storage path of.</param>
public string GetPathForFile(string filename) => Files?.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath;
public List<BeatmapSetFileInfo> Files { get; set; } public List<BeatmapSetFileInfo> Files { get; set; }
public override string ToString() => Metadata?.ToString() ?? base.ToString(); public override string ToString() => Metadata?.ToString() ?? base.ToString();

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading; using System.Threading;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -48,6 +49,8 @@ namespace osu.Game.Beatmaps
protected override Track GetBeatmapTrack() => GetVirtualTrack(); protected override Track GetBeatmapTrack() => GetVirtualTrack();
public override Stream GetStream(string storagePath) => null;
private class DummyRulesetInfo : RulesetInfo private class DummyRulesetInfo : RulesetInfo
{ {
public override Ruleset CreateInstance() => new DummyRuleset(); public override Ruleset CreateInstance() => new DummyRuleset();

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -41,6 +42,11 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
ISkin Skin { get; } ISkin Skin { get; }
/// <summary>
/// Retrieves the <see cref="Track"/> which this <see cref="WorkingBeatmap"/> has loaded.
/// </summary>
Track Track { get; }
/// <summary> /// <summary>
/// Constructs a playable <see cref="IBeatmap"/> from <see cref="Beatmap"/> using the applicable converters for a specific <see cref="RulesetInfo"/>. /// Constructs a playable <see cref="IBeatmap"/> from <see cref="Beatmap"/> using the applicable converters for a specific <see cref="RulesetInfo"/>.
/// <para> /// <para>
@ -67,5 +73,11 @@ namespace osu.Game.Beatmaps
/// </remarks> /// </remarks>
/// <returns>A fresh track instance, which will also be available via <see cref="Track"/>.</returns> /// <returns>A fresh track instance, which will also be available via <see cref="Track"/>.</returns>
Track LoadTrack(); Track LoadTrack();
/// <summary>
/// Returns the stream of the file from the given storage path.
/// </summary>
/// <param name="storagePath">The storage path to the file.</param>
Stream GetStream(string storagePath);
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -326,6 +327,8 @@ namespace osu.Game.Beatmaps
protected virtual ISkin GetSkin() => new DefaultSkin(); protected virtual ISkin GetSkin() => new DefaultSkin();
private readonly RecyclableLazy<ISkin> skin; private readonly RecyclableLazy<ISkin> skin;
public abstract Stream GetStream(string storagePath);
public class RecyclableLazy<T> public class RecyclableLazy<T>
{ {
private Lazy<T> lazy; private Lazy<T> lazy;

View File

@ -16,9 +16,18 @@ namespace osu.Game.Rulesets.Edit
{ {
private readonly List<ICheck> checks = new List<ICheck> private readonly List<ICheck> checks = new List<ICheck>
{ {
new CheckBackground(), // Resources
new CheckBackgroundPresence(),
new CheckBackgroundQuality(),
// Audio
new CheckAudioPresence(),
new CheckAudioQuality()
}; };
public IEnumerable<Issue> Run(IBeatmap beatmap) => checks.SelectMany(check => check.Run(beatmap)); public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)
{
return checks.SelectMany(check => check.Run(playableBeatmap, workingBeatmap));
}
} }
} }

View File

@ -0,0 +1,15 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckAudioPresence : CheckFilePresence
{
protected override CheckCategory Category => CheckCategory.Audio;
protected override string TypeOfFile => "audio";
protected override string GetFilename(IBeatmap playableBeatmap) => playableBeatmap.Metadata?.AudioFile;
}
}

View File

@ -0,0 +1,75 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckAudioQuality : ICheck
{
// This is a requirement as stated in the Ranking Criteria.
// See https://osu.ppy.sh/wiki/en/Ranking_Criteria#rules.4
private const int max_bitrate = 192;
// "A song's audio file /.../ must be of reasonable quality. Try to find the highest quality source file available"
// There not existing a version with a bitrate of 128 kbps or higher is extremely rare.
private const int min_bitrate = 128;
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Too high or low audio bitrate");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooHighBitrate(this),
new IssueTemplateTooLowBitrate(this),
new IssueTemplateNoBitrate(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
{
var audioFile = playableBeatmap.Metadata?.AudioFile;
if (audioFile == null)
yield break;
var track = workingBeatmap.Track;
if (track?.Bitrate == null || track.Bitrate.Value == 0)
yield return new IssueTemplateNoBitrate(this).Create();
else if (track.Bitrate.Value > max_bitrate)
yield return new IssueTemplateTooHighBitrate(this).Create(track.Bitrate.Value);
else if (track.Bitrate.Value < min_bitrate)
yield return new IssueTemplateTooLowBitrate(this).Create(track.Bitrate.Value);
}
public class IssueTemplateTooHighBitrate : IssueTemplate
{
public IssueTemplateTooHighBitrate(ICheck check)
: base(check, IssueType.Problem, "The audio bitrate ({0} kbps) exceeds {1} kbps.")
{
}
public Issue Create(int bitrate) => new Issue(this, bitrate, max_bitrate);
}
public class IssueTemplateTooLowBitrate : IssueTemplate
{
public IssueTemplateTooLowBitrate(ICheck check)
: base(check, IssueType.Problem, "The audio bitrate ({0} kbps) is lower than {1} kbps.")
{
}
public Issue Create(int bitrate) => new Issue(this, bitrate, min_bitrate);
}
public class IssueTemplateNoBitrate : IssueTemplate
{
public IssueTemplateNoBitrate(ICheck check)
: base(check, IssueType.Error, "The audio bitrate could not be retrieved.")
{
}
public Issue Create() => new Issue(this);
}
}
}

View File

@ -1,61 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckBackground : ICheck
{
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Resources, "Missing background");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateNoneSet(this),
new IssueTemplateDoesNotExist(this)
};
public IEnumerable<Issue> Run(IBeatmap beatmap)
{
if (beatmap.Metadata.BackgroundFile == null)
{
yield return new IssueTemplateNoneSet(this).Create();
yield break;
}
// If the background is set, also make sure it still exists.
var set = beatmap.BeatmapInfo.BeatmapSet;
var file = set.Files.FirstOrDefault(f => f.Filename == beatmap.Metadata.BackgroundFile);
if (file != null)
yield break;
yield return new IssueTemplateDoesNotExist(this).Create(beatmap.Metadata.BackgroundFile);
}
public class IssueTemplateNoneSet : IssueTemplate
{
public IssueTemplateNoneSet(ICheck check)
: base(check, IssueType.Problem, "No background has been set.")
{
}
public Issue Create() => new Issue(this);
}
public class IssueTemplateDoesNotExist : IssueTemplate
{
public IssueTemplateDoesNotExist(ICheck check)
: base(check, IssueType.Problem, "The background file \"{0}\" does not exist.")
{
}
public Issue Create(string filename) => new Issue(this, filename);
}
}
}

View File

@ -0,0 +1,15 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckBackgroundPresence : CheckFilePresence
{
protected override CheckCategory Category => CheckCategory.Resources;
protected override string TypeOfFile => "background";
protected override string GetFilename(IBeatmap playableBeatmap) => playableBeatmap.Metadata?.BackgroundFile;
}
}

View File

@ -0,0 +1,98 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckBackgroundQuality : ICheck
{
// These are the requirements as stated in the Ranking Criteria.
// See https://osu.ppy.sh/wiki/en/Ranking_Criteria#rules.5
private const int min_width = 160;
private const int max_width = 2560;
private const int min_height = 120;
private const int max_height = 1440;
private const double max_filesize_mb = 2.5d;
// It's usually possible to find a higher resolution of the same image if lower than these.
private const int low_width = 960;
private const int low_height = 540;
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Resources, "Too high or low background resolution");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooHighResolution(this),
new IssueTemplateTooLowResolution(this),
new IssueTemplateTooUncompressed(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
{
var backgroundFile = playableBeatmap.Metadata?.BackgroundFile;
if (backgroundFile == null)
yield break;
var texture = workingBeatmap.Background;
if (texture == null)
yield break;
if (texture.Width > max_width || texture.Height > max_height)
yield return new IssueTemplateTooHighResolution(this).Create(texture.Width, texture.Height);
if (texture.Width < min_width || texture.Height < min_height)
yield return new IssueTemplateTooLowResolution(this).Create(texture.Width, texture.Height);
else if (texture.Width < low_width || texture.Height < low_height)
yield return new IssueTemplateLowResolution(this).Create(texture.Width, texture.Height);
string storagePath = playableBeatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile);
double filesizeMb = workingBeatmap.GetStream(storagePath).Length / (1024d * 1024d);
if (filesizeMb > max_filesize_mb)
yield return new IssueTemplateTooUncompressed(this).Create(filesizeMb);
}
public class IssueTemplateTooHighResolution : IssueTemplate
{
public IssueTemplateTooHighResolution(ICheck check)
: base(check, IssueType.Problem, "The background resolution ({0} x {1}) exceeds {2} x {3}.")
{
}
public Issue Create(double width, double height) => new Issue(this, width, height, max_width, max_height);
}
public class IssueTemplateTooLowResolution : IssueTemplate
{
public IssueTemplateTooLowResolution(ICheck check)
: base(check, IssueType.Problem, "The background resolution ({0} x {1}) is lower than {2} x {3}.")
{
}
public Issue Create(double width, double height) => new Issue(this, width, height, min_width, min_height);
}
public class IssueTemplateLowResolution : IssueTemplate
{
public IssueTemplateLowResolution(ICheck check)
: base(check, IssueType.Warning, "The background resolution ({0} x {1}) is lower than {2} x {3}.")
{
}
public Issue Create(double width, double height) => new Issue(this, width, height, low_width, low_height);
}
public class IssueTemplateTooUncompressed : IssueTemplate
{
public IssueTemplateTooUncompressed(ICheck check)
: base(check, IssueType.Problem, "The background filesize ({0:0.##} MB) exceeds {1} MB.")
{
}
public Issue Create(double actualMb) => new Issue(this, actualMb, max_filesize_mb);
}
}
}

View File

@ -0,0 +1,63 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public abstract class CheckFilePresence : ICheck
{
protected abstract CheckCategory Category { get; }
protected abstract string TypeOfFile { get; }
protected abstract string GetFilename(IBeatmap playableBeatmap);
public CheckMetadata Metadata => new CheckMetadata(Category, $"Missing {TypeOfFile}");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateNoneSet(this),
new IssueTemplateDoesNotExist(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
{
var filename = GetFilename(playableBeatmap);
if (filename == null)
{
yield return new IssueTemplateNoneSet(this).Create(TypeOfFile);
yield break;
}
// If the file is set, also make sure it still exists.
var storagePath = playableBeatmap.BeatmapInfo.BeatmapSet.GetPathForFile(filename);
if (storagePath != null)
yield break;
yield return new IssueTemplateDoesNotExist(this).Create(TypeOfFile, filename);
}
public class IssueTemplateNoneSet : IssueTemplate
{
public IssueTemplateNoneSet(ICheck check)
: base(check, IssueType.Problem, "No {0} has been set.")
{
}
public Issue Create(string typeOfFile) => new Issue(this, typeOfFile);
}
public class IssueTemplateDoesNotExist : IssueTemplate
{
public IssueTemplateDoesNotExist(ICheck check)
: base(check, IssueType.Problem, "The {0} file \"{1}\" does not exist.")
{
}
public Issue Create(string typeOfFile, string filename) => new Issue(this, typeOfFile, filename);
}
}
}

View File

@ -24,7 +24,8 @@ namespace osu.Game.Rulesets.Edit.Checks.Components
/// <summary> /// <summary>
/// Runs this check and returns any issues detected for the provided beatmap. /// Runs this check and returns any issues detected for the provided beatmap.
/// </summary> /// </summary>
/// <param name="beatmap">The beatmap to run the check on.</param> /// <param name="playableBeatmap">The playable beatmap of the beatmap to run the check on.</param>
public IEnumerable<Issue> Run(IBeatmap beatmap); /// <param name="workingBeatmap">The working beatmap of the beatmap to run the check on.</param>
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap);
} }
} }

View File

@ -12,6 +12,6 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public interface IBeatmapVerifier public interface IBeatmapVerifier
{ {
public IEnumerable<Issue> Run(IBeatmap beatmap); public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap);
} }
} }

View File

@ -116,6 +116,8 @@ namespace osu.Game.Screens.Edit
protected override Texture GetBackground() => throw new NotImplementedException(); protected override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
} }
} }
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -60,7 +61,10 @@ namespace osu.Game.Screens.Edit.Verify
private EditorClock clock { get; set; } private EditorClock clock { get; set; }
[Resolved] [Resolved]
protected EditorBeatmap Beatmap { get; private set; } private IBindable<WorkingBeatmap> workingBeatmap { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved] [Resolved]
private Bindable<Issue> selectedIssue { get; set; } private Bindable<Issue> selectedIssue { get; set; }
@ -72,7 +76,7 @@ namespace osu.Game.Screens.Edit.Verify
private void load(OverlayColourProvider colours) private void load(OverlayColourProvider colours)
{ {
generalVerifier = new BeatmapVerifier(); generalVerifier = new BeatmapVerifier();
rulesetVerifier = Beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier(); rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier();
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -118,10 +122,10 @@ namespace osu.Game.Screens.Edit.Verify
private void refresh() private void refresh()
{ {
var issues = generalVerifier.Run(Beatmap); var issues = generalVerifier.Run(beatmap, workingBeatmap.Value);
if (rulesetVerifier != null) if (rulesetVerifier != null)
issues = issues.Concat(rulesetVerifier.Run(Beatmap)); issues = issues.Concat(rulesetVerifier.Run(beatmap, workingBeatmap.Value));
table.Issues = issues table.Issues = issues
.OrderBy(issue => issue.Template.Type) .OrderBy(issue => issue.Template.Type)

View File

@ -215,6 +215,8 @@ namespace osu.Game.Tests.Beatmaps
protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
protected override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) protected override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
{ {
var converter = base.CreateBeatmapConverter(beatmap, ruleset); var converter = base.CreateBeatmapConverter(beatmap, ruleset);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.IO;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
@ -35,6 +36,8 @@ namespace osu.Game.Tests.Beatmaps
protected override Storyboard GetStoryboard() => storyboard ?? base.GetStoryboard(); protected override Storyboard GetStoryboard() => storyboard ?? base.GetStoryboard();
public override Stream GetStream(string storagePath) => null;
protected override Texture GetBackground() => null; protected override Texture GetBackground() => null;
protected override Track GetBeatmapTrack() => null; protected override Track GetBeatmapTrack() => null;