// 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 System.Threading; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.NonVisual { [TestFixture] public class TestSceneTimedDifficultyCalculation { [Test] public void TestAttributesGeneratedForAllNonSkippedObjects() { var beatmap = new Beatmap { HitObjects = { new TestHitObject { StartTime = 1 }, new TestHitObject { StartTime = 2, Nested = 1 }, new TestHitObject { StartTime = 3 }, } }; List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); Assert.That(attribs.Count, Is.EqualTo(4)); assertEquals(attribs[0], beatmap.HitObjects[0]); assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]); assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1]); // From the nested object. assertEquals(attribs[3], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); } [Test] public void TestAttributesNotGeneratedForSkippedObjects() { var beatmap = new Beatmap { HitObjects = { // The first object is usually skipped in all implementations new TestHitObject { StartTime = 1, Skip = true }, // An intermediate skipped object. new TestHitObject { StartTime = 2, Skip = true }, new TestHitObject { StartTime = 3 }, } }; List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); Assert.That(attribs.Count, Is.EqualTo(1)); assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); } [Test] public void TestNestedObjectOnlyAddsParentOnce() { var beatmap = new Beatmap { HitObjects = { new TestHitObject { StartTime = 1, Skip = true, Nested = 2 }, } }; List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); Assert.That(attribs.Count, Is.EqualTo(2)); assertEquals(attribs[0], beatmap.HitObjects[0]); assertEquals(attribs[1], beatmap.HitObjects[0]); } [Test] public void TestSkippedLastObjectAddedInLastIteration() { var beatmap = new Beatmap { HitObjects = { new TestHitObject { StartTime = 1 }, new TestHitObject { StartTime = 2, Skip = true }, new TestHitObject { StartTime = 3, Skip = true }, } }; List attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed(); Assert.That(attribs.Count, Is.EqualTo(1)); assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]); } private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected) { Assert.That(((TestDifficultyAttributes)attribs.Attributes).Objects, Is.EquivalentTo(expected)); } private class TestHitObject : HitObject { /// /// Whether to skip generating a difficulty representation for this object. /// public bool Skip { get; set; } /// /// Whether to generate nested difficulty representations for this object, and if so, how many. /// public int Nested { get; set; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { for (int i = 0; i < Nested; i++) AddNested(new TestHitObject { StartTime = StartTime + 0.1 * i }); } } private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => Enumerable.Empty(); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => throw new NotImplementedException(); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new PassThroughBeatmapConverter(beatmap); public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TestDifficultyCalculator(beatmap); public override string Description => string.Empty; public override string ShortName => string.Empty; private class PassThroughBeatmapConverter : IBeatmapConverter { public event Action>? ObjectConverted { add { } remove { } } public IBeatmap Beatmap { get; } public PassThroughBeatmapConverter(IBeatmap beatmap) { Beatmap = beatmap; } public bool CanConvert() => true; public IBeatmap Convert(CancellationToken cancellationToken = default) => Beatmap; } } private class TestDifficultyCalculator : DifficultyCalculator { public TestDifficultyCalculator(IWorkingBeatmap beatmap) : base(new TestRuleset().RulesetInfo, beatmap) { } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) => new TestDifficultyAttributes { Objects = beatmap.HitObjects.ToArray() }; protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List objects = new List(); foreach (var obj in beatmap.HitObjects.OfType()) { if (!obj.Skip) objects.Add(new DifficultyHitObject(obj, obj, clockRate, objects, objects.Count)); foreach (var nested in obj.NestedHitObjects) objects.Add(new DifficultyHitObject(nested, nested, clockRate, objects, objects.Count)); } return objects; } protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new PassThroughSkill(mods) }; private class PassThroughSkill : Skill { public PassThroughSkill(Mod[] mods) : base(mods) { } public override void Process(DifficultyHitObject current) { } public override double DifficultyValue() => 1; } } private class TestDifficultyAttributes : DifficultyAttributes { public HitObject[] Objects = Array.Empty(); } } }