diff --git a/osu.Game.Tests/Editing/Checks/CheckInconsistentMetadataTest.cs b/osu.Game.Tests/Editing/Checks/CheckInconsistentMetadataTest.cs new file mode 100644 index 0000000000..ebf90766b2 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckInconsistentMetadataTest.cs @@ -0,0 +1,217 @@ +// Copyright (c) ppy Pty Ltd . 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.Game.Beatmaps; +using osu.Game.Models; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckInconsistentMetadataTest + { + private CheckInconsistentMetadata check = null!; + + [SetUp] + public void Setup() + { + check = new CheckInconsistentMetadata(); + } + + [Test] + public void TestConsistentMetadata() + { + var metadata = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag2"); + var beatmaps = createBeatmapSetWithMetadata(metadata, metadata); + var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps); + + Assert.That(check.Run(context), Is.Empty); + } + + [Test] + public void TestInconsistentArtist() + { + var metadata1 = createMetadata("Artist One", "Test Title", "Test Source", "Test Creator", "tag1 tag2"); + var metadata2 = createMetadata("Artist Two", "Test Title", "Test Source", "Test Creator", "tag1 tag2"); + var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2); + var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields); + Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent artist fields")); + Assert.That(issues[0].ToString(), Contains.Substring("Artist One")); + Assert.That(issues[0].ToString(), Contains.Substring("Artist Two")); + } + + [Test] + public void TestInconsistentTitle() + { + var metadata1 = createMetadata("Test Artist", "Title One", "Test Source", "Test Creator", "tag1 tag2"); + var metadata2 = createMetadata("Test Artist", "Title Two", "Test Source", "Test Creator", "tag1 tag2"); + var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2); + var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields); + Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent title fields")); + } + + [Test] + public void TestInconsistentUnicodeArtist() + { + var metadata1 = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag2", unicodeArtist: "Test Unicode Artist 1"); + var metadata2 = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag2", unicodeArtist: "Test Unicode Artist 2"); + var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2); + var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields); + Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent unicode artist fields")); + } + + [Test] + public void TestInconsistentSource() + { + var metadata1 = createMetadata("Test Artist", "Test Title", "Source One", "Test Creator", "tag1 tag2"); + var metadata2 = createMetadata("Test Artist", "Test Title", "Source Two", "Test Creator", "tag1 tag2"); + var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2); + var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields); + Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent source fields")); + } + + [Test] + public void TestInconsistentCreator() + { + var metadata1 = createMetadata("Test Artist", "Test Title", "Test Source", "Creator One", "tag1 tag2"); + var metadata2 = createMetadata("Test Artist", "Test Title", "Test Source", "Creator Two", "tag1 tag2"); + var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2); + var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields); + Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent creator fields")); + } + + [Test] + public void TestInconsistentTags() + { + var metadata1 = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag2 tag3"); + var metadata2 = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag4 tag5"); + var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2); + var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues[0].Template is CheckInconsistentMetadata.IssueTemplateInconsistentTags); + Assert.That(issues[0].ToString(), Contains.Substring("Inconsistent tags")); + Assert.That(issues[0].ToString(), Contains.Substring("tag2 tag3 tag4 tag5")); + } + + [Test] + public void TestMultipleInconsistencies() + { + var metadata1 = createMetadata("Artist One", "Title One", "Test Source", "Test Creator", "tag1 tag2"); + var metadata2 = createMetadata("Artist Two", "Title Two", "Test Source", "Test Creator", "tag3 tag4"); + var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2); + var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps); + + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(3)); // artist, title, tags + Assert.That(issues.Count(i => i.Template is CheckInconsistentMetadata.IssueTemplateInconsistentOtherFields), Is.EqualTo(2)); + Assert.That(issues.Count(i => i.Template is CheckInconsistentMetadata.IssueTemplateInconsistentTags), Is.EqualTo(1)); + } + + [Test] + public void TestSingleDifficulty() + { + var metadata = createMetadata("Test Artist", "Test Title", "Test Source", "Test Creator", "tag1 tag2"); + var beatmaps = createBeatmapSetWithMetadata(metadata); + var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps); + + Assert.That(check.Run(context), Is.Empty); + } + + [Test] + public void TestEmptyStringFieldsAreConsistent() + { + var metadata1 = createMetadata("Test Artist", "Test Title", "", "Test Creator", ""); + var metadata2 = createMetadata("Test Artist", "Test Title", "", "Test Creator", ""); + var beatmaps = createBeatmapSetWithMetadata(metadata1, metadata2); + var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps); + + Assert.That(check.Run(context), Is.Empty); + } + + private BeatmapMetadata createMetadata(string artist, string title, string source, string creator, string tags, string unicodeArtist = "", string unicodeTitle = "") + { + return new BeatmapMetadata(new RealmUser { Username = creator }) + { + Artist = artist, + Title = title, + Source = source, + Tags = tags, + ArtistUnicode = unicodeArtist, + TitleUnicode = unicodeTitle + }; + } + + private IBeatmap[] createBeatmapSetWithMetadata(params BeatmapMetadata[] metadata) + { + var beatmapSet = new BeatmapSetInfo(); + var beatmaps = new IBeatmap[metadata.Length]; + + for (int i = 0; i < metadata.Length; i++) + { + beatmaps[i] = createBeatmapWithMetadata(metadata[i], $"Difficulty {i + 1}"); + beatmaps[i].BeatmapInfo.BeatmapSet = beatmapSet; + } + + // Configure the beatmapset to contain all the beatmap infos + foreach (var beatmap in beatmaps) + beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); + + return beatmaps; + } + + private Beatmap createBeatmapWithMetadata(BeatmapMetadata metadata, string difficultyName) + { + return new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + DifficultyName = difficultyName, + Metadata = metadata + } + }; + } + + private BeatmapVerifierContext createContextWithMultipleDifficulties(IBeatmap currentBeatmap, IBeatmap[] allDifficulties) + { + return new BeatmapVerifierContext( + currentBeatmap, + new TestWorkingBeatmap(currentBeatmap), + DifficultyRating.ExpertPlus, + beatmapInfo => allDifficulties.FirstOrDefault(b => b.BeatmapInfo.Equals(beatmapInfo)) + ); + } + } +} diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index e1c0815dac..868835342a 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Edit // Metadata new CheckTitleMarkers(), + new CheckInconsistentMetadata(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs new file mode 100644 index 0000000000..cf74ca3ea3 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckInconsistentMetadata.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . 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.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckInconsistentMetadata : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Metadata, "Inconsistent metadata"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateInconsistentTags(this), + new IssueTemplateInconsistentOtherFields(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var difficulties = context.BeatmapsetDifficulties; + + if (difficulties.Count <= 1) + yield break; + + var referenceBeatmap = difficulties.OrderByDescending(b => b.BeatmapInfo.StarRating).First(); + var referenceMetadata = referenceBeatmap.Metadata; + + // Define metadata fields to check + var fieldsToCheck = new (string fieldName, Func fieldSelector)[] + { + ("artist", m => m.Artist), + ("unicode artist", m => m.ArtistUnicode), + ("title", m => m.Title), + ("unicode title", m => m.TitleUnicode), + ("source", m => m.Source), + ("creator", m => m.Author.Username) + }; + + foreach (var beatmap in difficulties) + { + if (beatmap == referenceBeatmap) + continue; + + var currentMetadata = beatmap.Metadata; + + // Check each metadata field for inconsistencies + foreach ((string fieldName, var fieldSelector) in fieldsToCheck) + { + string referenceField = fieldSelector(referenceMetadata); + string currentField = fieldSelector(currentMetadata); + + if (referenceField != currentField) + { + yield return new IssueTemplateInconsistentOtherFields(this).Create( + fieldName, + referenceBeatmap.BeatmapInfo.DifficultyName, + beatmap.BeatmapInfo.DifficultyName, + referenceField, + currentField + ); + } + } + + // Special handling for tags + if (referenceMetadata.Tags != currentMetadata.Tags) + { + var differenceTags = referenceMetadata.Tags.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToHashSet(); + differenceTags.SymmetricExceptWith(currentMetadata.Tags.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + string difference = string.Join(" ", differenceTags); + + if (!string.IsNullOrEmpty(difference)) + { + yield return new IssueTemplateInconsistentTags(this).Create( + referenceBeatmap.BeatmapInfo.DifficultyName, + beatmap.BeatmapInfo.DifficultyName, + difference + ); + } + } + } + } + + public class IssueTemplateInconsistentTags : IssueTemplate + { + public IssueTemplateInconsistentTags(ICheck check) + : base(check, IssueType.Problem, "Inconsistent tags between \"{0}\" and \"{1}\", difference being \"{2}\".") + { + } + + public Issue Create(string referenceDifficulty, string currentDifficulty, string difference) + => new Issue(this, referenceDifficulty, currentDifficulty, difference); + } + + public class IssueTemplateInconsistentOtherFields : IssueTemplate + { + public IssueTemplateInconsistentOtherFields(ICheck check) + : base(check, IssueType.Problem, "Inconsistent {0} fields between \"{1}\" and \"{2}\"; \"{3}\" and \"{4}\" respectively.") + { + } + + public Issue Create(string fieldName, string referenceDifficulty, string currentDifficulty, string referenceValue, string currentValue) + => new Issue(this, fieldName, referenceDifficulty, currentDifficulty, referenceValue, currentValue); + } + } +}