1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 03:22:57 +08:00

Merge branch 'master' into localisable-tooltips

This commit is contained in:
Dean Herbert 2021-06-28 15:29:42 +09:00
commit f939e2603a
19 changed files with 1234 additions and 17 deletions

View File

@ -0,0 +1,241 @@
// 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 NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckFewHitsoundsTest
{
private CheckFewHitsounds check;
private List<HitSampleInfo> notHitsounded;
private List<HitSampleInfo> hitsounded;
[SetUp]
public void Setup()
{
check = new CheckFewHitsounds();
notHitsounded = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
hitsounded = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL),
new HitSampleInfo(HitSampleInfo.HIT_FINISH)
};
}
[Test]
public void TestHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 16; ++i)
{
var samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
if ((i + 1) % 2 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
if ((i + 1) % 3 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
if ((i + 1) % 4 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
assertOk(hitObjects);
}
[Test]
public void TestHitsoundedWithBreak()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 32; ++i)
{
var samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
if ((i + 1) % 2 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
if ((i + 1) % 3 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
if ((i + 1) % 4 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
// Leaves a gap in which no hitsounds exist or can be added, and so shouldn't be an issue.
if (i > 8 && i < 24)
continue;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
assertOk(hitObjects);
}
[Test]
public void TestLightlyHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 30; ++i)
{
var samples = i % 8 == 0 ? hitsounded : notHitsounded;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
assertLongPeriodNegligible(hitObjects, count: 3);
}
[Test]
public void TestRarelyHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 30; ++i)
{
var samples = (i == 0 || i == 15) ? hitsounded : notHitsounded;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
// Should prompt one warning between 1st and 16th, and another between 16th and 31st.
assertLongPeriodWarning(hitObjects, count: 2);
}
[Test]
public void TestExtremelyRarelyHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 80; ++i)
{
var samples = i == 40 ? hitsounded : notHitsounded;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
// Should prompt one problem between 1st and 41st, and another between 41st and 81st.
assertLongPeriodProblem(hitObjects, count: 2);
}
[Test]
public void TestNotHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 20; ++i)
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = notHitsounded });
assertNoHitsounds(hitObjects);
}
[Test]
public void TestNestedObjectsHitsounded()
{
var ticks = new List<HitObject>();
for (int i = 1; i < 16; ++i)
ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = hitsounded });
var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
{
Samples = hitsounded
};
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { nested });
}
[Test]
public void TestNestedObjectsRarelyHitsounded()
{
var ticks = new List<HitObject>();
for (int i = 1; i < 16; ++i)
ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = i == 0 ? hitsounded : notHitsounded });
var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
{
Samples = hitsounded
};
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertLongPeriodWarning(new List<HitObject> { nested });
}
[Test]
public void TestConcurrentObjects()
{
var hitObjects = new List<HitObject>();
var ticks = new List<HitObject>();
for (int i = 1; i < 10; ++i)
ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded });
var nested = new MockNestableHitObject(ticks.ToList(), 0, 50000)
{
Samples = notHitsounded
};
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
hitObjects.Add(nested);
for (int i = 1; i <= 6; ++i)
hitObjects.Add(new HitCircle { StartTime = 10000 * i, Samples = notHitsounded });
assertOk(hitObjects);
}
private void assertOk(List<HitObject> hitObjects)
{
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
}
private void assertLongPeriodProblem(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodProblem));
}
private void assertLongPeriodWarning(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodWarning));
}
private void assertLongPeriodNegligible(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodNegligible));
}
private void assertNoHitsounds(List<HitObject> hitObjects)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds));
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
{
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,289 @@
// 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 NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckMutedObjectsTest
{
private CheckMutedObjects check;
private ControlPointInfo cpi;
private const int volume_regular = 50;
private const int volume_low = 15;
private const int volume_muted = 5;
[SetUp]
public void Setup()
{
check = new CheckMutedObjects();
cpi = new ControlPointInfo();
cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
}
[Test]
public void TestNormalControlPointVolume()
{
var hitcircle = new HitCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { hitcircle });
}
[Test]
public void TestLowControlPointVolume()
{
var hitcircle = new HitCircle
{
StartTime = 1000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertLowVolume(new List<HitObject> { hitcircle });
}
[Test]
public void TestMutedControlPointVolume()
{
var hitcircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { hitcircle });
}
[Test]
public void TestNormalSampleVolume()
{
// The sample volume should take precedence over the control point volume.
var hitcircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { hitcircle });
}
[Test]
public void TestLowSampleVolume()
{
var hitcircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_low) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertLowVolume(new List<HitObject> { hitcircle });
}
[Test]
public void TestMutedSampleVolume()
{
var hitcircle = new HitCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { hitcircle });
}
[Test]
public void TestNormalSampleVolumeSlider()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_muted) } // Should be fine.
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { slider });
}
[Test]
public void TestMutedSampleVolumeSliderHead()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail.
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { slider });
}
[Test]
public void TestMutedSampleVolumeSliderTail()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } // Applies to the tail.
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMutedPassive(new List<HitObject> { slider });
}
[Test]
public void TestMutedControlPointVolumeSliderHead()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 2250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { slider });
}
[Test]
public void TestMutedControlPointVolumeSliderTail()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
// Ends after the 5% control point.
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMutedPassive(new List<HitObject> { slider });
}
private void assertOk(List<HitObject> hitObjects)
{
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
}
private void assertLowVolume(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateLowVolumeActive));
}
private void assertMuted(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedActive));
}
private void assertMutedPassive(List<HitObject> hitObjects)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Any(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedPassive));
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
{
var beatmap = new Beatmap<HitObject>
{
ControlPointInfo = cpi,
HitObjects = hitObjects
};
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,36 @@
// 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.Threading;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Tests.Editing.Checks
{
public sealed class MockNestableHitObject : HitObject, IHasDuration
{
private readonly IEnumerable<HitObject> toBeNested;
public MockNestableHitObject(IEnumerable<HitObject> toBeNested, double startTime, double endTime)
{
this.toBeNested = toBeNested;
StartTime = startTime;
EndTime = endTime;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
foreach (var hitObject in toBeNested)
AddNested(hitObject);
}
public double EndTime { get; }
public double Duration
{
get => EndTime - StartTime;
set => throw new System.NotImplementedException();
}
}
}

View File

@ -90,6 +90,20 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
}
[Test]
public void TestApplyOverallDifficultyQueries()
{
const string query = "od>4 easy od<8";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.Greater(filterCriteria.OverallDifficulty.Min, 4.0);
Assert.Less(filterCriteria.OverallDifficulty.Min, 4.1);
Assert.Greater(filterCriteria.OverallDifficulty.Max, 7.9);
Assert.Less(filterCriteria.OverallDifficulty.Max, 8.0);
}
[Test]
public void TestApplyBPMQueries()
{

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 System.Linq;
using NUnit.Framework;
@ -14,6 +15,8 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
@ -23,6 +26,8 @@ namespace osu.Game.Tests.Visual.Online
private BeatmapListingOverlay overlay;
private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType<BeatmapListingSearchControl>().Single();
[BackgroundDependencyLoader]
private void load()
{
@ -39,6 +44,16 @@ namespace osu.Game.Tests.Visual.Online
return true;
};
AddStep("initialize dummy", () =>
{
// non-supporter user
((DummyAPIAccess)API).LocalUser.Value = new User
{
Username = "TestBot",
Id = API.LocalUser.Value.Id + 1,
};
});
}
[Test]
@ -58,13 +73,164 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
}
[Test]
public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithoutResults()
{
AddStep("fetch for 0 beatmaps", () => fetchFor());
AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
// only Rank Achieved filter
setRankAchievedFilter(new[] { ScoreRank.XH });
supporterRequiredPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
notFoundPlaceholderShown();
// only Played filter
setPlayedFilter(SearchPlayed.Played);
supporterRequiredPlaceholderShown();
setPlayedFilter(SearchPlayed.Any);
notFoundPlaceholderShown();
// both RankAchieved and Played filters
setRankAchievedFilter(new[] { ScoreRank.XH });
setPlayedFilter(SearchPlayed.Played);
supporterRequiredPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
setPlayedFilter(SearchPlayed.Any);
notFoundPlaceholderShown();
}
[Test]
public void TestUserWithSupporterUsesSupporterOnlyFiltersWithoutResults()
{
AddStep("fetch for 0 beatmaps", () => fetchFor());
AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
// only Rank Achieved filter
setRankAchievedFilter(new[] { ScoreRank.XH });
notFoundPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
notFoundPlaceholderShown();
// only Played filter
setPlayedFilter(SearchPlayed.Played);
notFoundPlaceholderShown();
setPlayedFilter(SearchPlayed.Any);
notFoundPlaceholderShown();
// both Rank Achieved and Played filters
setRankAchievedFilter(new[] { ScoreRank.XH });
setPlayedFilter(SearchPlayed.Played);
notFoundPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
setPlayedFilter(SearchPlayed.Any);
notFoundPlaceholderShown();
}
[Test]
public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithResults()
{
AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
// only Rank Achieved filter
setRankAchievedFilter(new[] { ScoreRank.XH });
supporterRequiredPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
noPlaceholderShown();
// only Played filter
setPlayedFilter(SearchPlayed.Played);
supporterRequiredPlaceholderShown();
setPlayedFilter(SearchPlayed.Any);
noPlaceholderShown();
// both Rank Achieved and Played filters
setRankAchievedFilter(new[] { ScoreRank.XH });
setPlayedFilter(SearchPlayed.Played);
supporterRequiredPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
setPlayedFilter(SearchPlayed.Any);
noPlaceholderShown();
}
[Test]
public void TestUserWithSupporterUsesSupporterOnlyFiltersWithResults()
{
AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
// only Rank Achieved filter
setRankAchievedFilter(new[] { ScoreRank.XH });
noPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
noPlaceholderShown();
// only Played filter
setPlayedFilter(SearchPlayed.Played);
noPlaceholderShown();
setPlayedFilter(SearchPlayed.Any);
noPlaceholderShown();
// both Rank Achieved and Played filters
setRankAchievedFilter(new[] { ScoreRank.XH });
setPlayedFilter(SearchPlayed.Played);
noPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
setPlayedFilter(SearchPlayed.Any);
noPlaceholderShown();
}
private void fetchFor(params BeatmapSetInfo[] beatmaps)
{
setsForResponse.Clear();
setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
// trigger arbitrary change for fetching.
overlay.ChildrenOfType<BeatmapListingSearchControl>().Single().Query.TriggerChange();
searchControl.Query.TriggerChange();
}
private void setRankAchievedFilter(ScoreRank[] ranks)
{
AddStep($"set Rank Achieved filter to [{string.Join(',', ranks)}]", () =>
{
searchControl.Ranks.Clear();
searchControl.Ranks.AddRange(ranks);
});
}
private void setPlayedFilter(SearchPlayed played)
{
AddStep($"set Played filter to {played}", () => searchControl.Played.Value = played);
}
private void supporterRequiredPlaceholderShown()
{
AddUntilStep("\"supporter required\" placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().SingleOrDefault()?.IsPresent == true);
}
private void notFoundPlaceholderShown()
{
AddUntilStep("\"no maps found\" placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
}
private void noPlaceholderShown()
{
AddUntilStep("no placeholder shown", () =>
!overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().Any()
&& !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any());
}
private class TestAPIBeatmapSet : APIBeatmapSet

View File

@ -191,8 +191,6 @@ namespace osu.Game.Beatmaps
{
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
LogForModel(beatmapSet, $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps...");
// ensure all IDs are unique
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
{

View File

@ -48,7 +48,6 @@ namespace osu.Game.Beatmaps
public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
{
LogForModel(beatmapSet, "Performing online lookups...");
return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
}

View File

@ -727,7 +727,7 @@ namespace osu.Game.Database
/// <param name="model">The model to populate.</param>
/// <param name="archive">The archive to use as a reference for population. May be null.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
protected virtual Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default) => Task.CompletedTask;
protected abstract Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default);
/// <summary>
/// Perform any final actions before the import to database executes.

View File

@ -10,11 +10,13 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@ -23,9 +25,9 @@ namespace osu.Game.Overlays.BeatmapListing
public class BeatmapListingFilterControl : CompositeDrawable
{
/// <summary>
/// Fired when a search finishes. Contains only new items in the case of pagination.
/// Fired when a search finishes.
/// </summary>
public Action<List<BeatmapSetInfo>> SearchFinished;
public Action<SearchResult> SearchFinished;
/// <summary>
/// Fired when search criteria change.
@ -212,7 +214,25 @@ namespace osu.Game.Overlays.BeatmapListing
lastResponse = response;
getSetsRequest = null;
SearchFinished?.Invoke(sets);
// check if a non-supporter used supporter-only filters
if (!api.LocalUser.Value.IsSupporter)
{
List<LocalisableString> filters = new List<LocalisableString>();
if (searchControl.Played.Value != SearchPlayed.Any)
filters.Add(BeatmapsStrings.ListingSearchFiltersPlayed);
if (searchControl.Ranks.Any())
filters.Add(BeatmapsStrings.ListingSearchFiltersRank);
if (filters.Any())
{
SearchFinished?.Invoke(SearchResult.SupporterOnlyFilters(filters));
return;
}
}
SearchFinished?.Invoke(SearchResult.ResultsReturned(sets));
};
api.Queue(getSetsRequest);
@ -237,5 +257,53 @@ namespace osu.Game.Overlays.BeatmapListing
base.Dispose(isDisposing);
}
/// <summary>
/// Indicates the type of result of a user-requested beatmap search.
/// </summary>
public enum SearchResultType
{
/// <summary>
/// Actual results have been returned from API.
/// </summary>
ResultsReturned,
/// <summary>
/// The user is not a supporter, but used supporter-only search filters.
/// </summary>
SupporterOnlyFilters
}
/// <summary>
/// Describes the result of a user-requested beatmap search.
/// </summary>
public struct SearchResult
{
public SearchResultType Type { get; private set; }
/// <summary>
/// Contains the beatmap sets returned from API.
/// Valid for read if and only if <see cref="Type"/> is <see cref="SearchResultType.ResultsReturned"/>.
/// </summary>
public List<BeatmapSetInfo> Results { get; private set; }
/// <summary>
/// Contains the names of supporter-only filters requested by the user.
/// Valid for read if and only if <see cref="Type"/> is <see cref="SearchResultType.SupporterOnlyFilters"/>.
/// </summary>
public List<LocalisableString> SupporterOnlyFiltersUsed { get; private set; }
public static SearchResult ResultsReturned(List<BeatmapSetInfo> results) => new SearchResult
{
Type = SearchResultType.ResultsReturned,
Results = results
};
public static SearchResult SupporterOnlyFilters(List<LocalisableString> filters) => new SearchResult
{
Type = SearchResultType.SupporterOnlyFilters,
SupporterOnlyFiltersUsed = filters
};
}
}
}

View File

@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Localisation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -15,7 +16,9 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Resources.Localisation.Web;
@ -33,6 +36,7 @@ namespace osu.Game.Overlays
private Container panelTarget;
private FillFlowContainer<BeatmapPanel> foundContent;
private NotFoundDrawable notFoundContent;
private SupporterRequiredDrawable supporterRequiredContent;
private BeatmapListingFilterControl filterControl;
public BeatmapListingOverlay()
@ -76,6 +80,7 @@ namespace osu.Game.Overlays
{
foundContent = new FillFlowContainer<BeatmapPanel>(),
notFoundContent = new NotFoundDrawable(),
supporterRequiredContent = new SupporterRequiredDrawable(),
}
}
},
@ -115,9 +120,16 @@ namespace osu.Game.Overlays
private Task panelLoadDelegate;
private void onSearchFinished(List<BeatmapSetInfo> beatmaps)
private void onSearchFinished(BeatmapListingFilterControl.SearchResult searchResult)
{
var newPanels = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters)
{
supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed);
addContentToPlaceholder(supporterRequiredContent);
return;
}
var newPanels = searchResult.Results.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@ -128,7 +140,7 @@ namespace osu.Game.Overlays
//No matches case
if (!newPanels.Any())
{
LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
addContentToPlaceholder(notFoundContent);
return;
}
@ -170,9 +182,9 @@ namespace osu.Game.Overlays
{
var transform = lastContent.FadeOut(100, Easing.OutQuint);
if (lastContent == notFoundContent)
if (lastContent == notFoundContent || lastContent == supporterRequiredContent)
{
// not found display may be used multiple times, so don't expire/dispose it.
// the placeholders may be used multiple times, so don't expire/dispose them.
transform.Schedule(() => panelTarget.Remove(lastContent));
}
else
@ -240,6 +252,67 @@ namespace osu.Game.Overlays
}
}
// TODO: localisation requires Text/LinkFlowContainer support for localising strings with links inside
// (https://github.com/ppy/osu-framework/issues/4530)
public class SupporterRequiredDrawable : CompositeDrawable
{
private LinkFlowContainer supporterRequiredText;
public SupporterRequiredDrawable()
{
RelativeSizeAxes = Axes.X;
Height = 225;
Alpha = 0;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
AddInternal(new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Texture = textures.Get(@"Online/supporter-required"),
},
supporterRequiredText = new LinkFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Bottom = 10 },
},
}
});
}
public void UpdateText(List<LocalisableString> filters)
{
supporterRequiredText.Clear();
supporterRequiredText.AddText(
BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(),
t =>
{
t.Font = OsuFont.GetFont(size: 16);
t.Colour = Colour4.White;
}
);
supporterRequiredText.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), @"/store/products/supporter-tag");
}
}
private const double time_between_fetches = 500;
private double lastFetchDisplayedTime;

View File

@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Edit
// Audio
new CheckAudioPresence(),
new CheckAudioQuality(),
new CheckMutedObjects(),
new CheckFewHitsounds(),
// Compose
new CheckUnsnappedObjects(),

View File

@ -0,0 +1,164 @@
// 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.Audio;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckFewHitsounds : ICheck
{
/// <summary>
/// 2 measures (4/4) of 120 BPM, typically makes up a few patterns in the map.
/// This is almost always ok, but can still be useful for the mapper to make sure hitsounding coverage is good.
/// </summary>
private const int negligible_threshold_time = 4000;
/// <summary>
/// 4 measures (4/4) of 120 BPM, typically makes up a large portion of a section in the song.
/// This is ok if the section is a quiet intro, for example.
/// </summary>
private const int warning_threshold_time = 8000;
/// <summary>
/// 12 measures (4/4) of 120 BPM, typically makes up multiple sections in the song.
/// </summary>
private const int problem_threshold_time = 24000;
// Should pass at least this many objects without hitsounds to be considered an issue (should work for Easy diffs too).
private const int warning_threshold_objects = 4;
private const int problem_threshold_objects = 16;
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Few or no hitsounds");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateLongPeriodProblem(this),
new IssueTemplateLongPeriodWarning(this),
new IssueTemplateLongPeriodNegligible(this),
new IssueTemplateNoHitsounds(this)
};
private bool mapHasHitsounds;
private int objectsWithoutHitsounds;
private double lastHitsoundTime;
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
if (!context.Beatmap.HitObjects.Any())
yield break;
mapHasHitsounds = false;
objectsWithoutHitsounds = 0;
lastHitsoundTime = context.Beatmap.HitObjects.First().StartTime;
var hitObjectsIncludingNested = new List<HitObject>();
foreach (var hitObject in context.Beatmap.HitObjects)
{
// Samples play on the end of objects. Some objects have nested objects to accomplish playing them elsewhere (e.g. slider head/repeat).
foreach (var nestedHitObject in hitObject.NestedHitObjects)
hitObjectsIncludingNested.Add(nestedHitObject);
hitObjectsIncludingNested.Add(hitObject);
}
var hitObjectsByEndTime = hitObjectsIncludingNested.OrderBy(o => o.GetEndTime()).ToList();
var hitObjectCount = hitObjectsByEndTime.Count;
for (int i = 0; i < hitObjectCount; ++i)
{
var hitObject = hitObjectsByEndTime[i];
// This is used to perform an update at the end so that the period after the last hitsounded object can be an issue.
bool isLastObject = i == hitObjectCount - 1;
foreach (var issue in applyHitsoundUpdate(hitObject, isLastObject))
yield return issue;
}
if (!mapHasHitsounds)
yield return new IssueTemplateNoHitsounds(this).Create();
}
private IEnumerable<Issue> applyHitsoundUpdate(HitObject hitObject, bool isLastObject = false)
{
var time = hitObject.GetEndTime();
bool hasHitsound = hitObject.Samples.Any(isHitsound);
bool couldHaveHitsound = hitObject.Samples.Any(isHitnormal);
// Only generating issues on hitsounded or last objects ensures we get one issue per long period.
// If there are no hitsounds we let the "No hitsounds" template take precedence.
if (hasHitsound || (isLastObject && mapHasHitsounds))
{
var timeWithoutHitsounds = time - lastHitsoundTime;
if (timeWithoutHitsounds > problem_threshold_time && objectsWithoutHitsounds > problem_threshold_objects)
yield return new IssueTemplateLongPeriodProblem(this).Create(lastHitsoundTime, timeWithoutHitsounds);
else if (timeWithoutHitsounds > warning_threshold_time && objectsWithoutHitsounds > warning_threshold_objects)
yield return new IssueTemplateLongPeriodWarning(this).Create(lastHitsoundTime, timeWithoutHitsounds);
else if (timeWithoutHitsounds > negligible_threshold_time && objectsWithoutHitsounds > warning_threshold_objects)
yield return new IssueTemplateLongPeriodNegligible(this).Create(lastHitsoundTime, timeWithoutHitsounds);
}
if (hasHitsound)
{
mapHasHitsounds = true;
objectsWithoutHitsounds = 0;
lastHitsoundTime = time;
}
else if (couldHaveHitsound)
++objectsWithoutHitsounds;
}
private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.Any(sample.Name.Contains);
private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL);
public abstract class IssueTemplateLongPeriod : IssueTemplate
{
protected IssueTemplateLongPeriod(ICheck check, IssueType type)
: base(check, type, "Long period without hitsounds ({0:F1} seconds).")
{
}
public Issue Create(double time, double duration) => new Issue(this, duration / 1000f) { Time = time };
}
public class IssueTemplateLongPeriodProblem : IssueTemplateLongPeriod
{
public IssueTemplateLongPeriodProblem(ICheck check)
: base(check, IssueType.Problem)
{
}
}
public class IssueTemplateLongPeriodWarning : IssueTemplateLongPeriod
{
public IssueTemplateLongPeriodWarning(ICheck check)
: base(check, IssueType.Warning)
{
}
}
public class IssueTemplateLongPeriodNegligible : IssueTemplateLongPeriod
{
public IssueTemplateLongPeriodNegligible(ICheck check)
: base(check, IssueType.Negligible)
{
}
}
public class IssueTemplateNoHitsounds : IssueTemplate
{
public IssueTemplateNoHitsounds(ICheck check)
: base(check, IssueType.Problem, "There are no hitsounds.")
{
}
public Issue Create() => new Issue(this);
}
}
}

View File

@ -0,0 +1,158 @@
// 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.Linq;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckMutedObjects : ICheck
{
/// <summary>
/// Volume percentages lower than or equal to this are typically inaudible.
/// </summary>
private const int muted_threshold = 5;
/// <summary>
/// Volume percentages lower than or equal to this can sometimes be inaudible depending on sample used and music volume.
/// </summary>
private const int low_volume_threshold = 20;
private enum EdgeType
{
Head,
Repeat,
Tail,
None
}
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Low volume hitobjects");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateMutedActive(this),
new IssueTemplateLowVolumeActive(this),
new IssueTemplateMutedPassive(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
foreach (var hitObject in context.Beatmap.HitObjects)
{
// Worth keeping in mind: The samples of an object always play at its end time.
// Objects like spinners have no sound at its start because of this, while hold notes have nested objects to accomplish this.
foreach (var nestedHitObject in hitObject.NestedHitObjects)
{
foreach (var issue in getVolumeIssues(hitObject, nestedHitObject))
yield return issue;
}
foreach (var issue in getVolumeIssues(hitObject))
yield return issue;
}
}
private IEnumerable<Issue> getVolumeIssues(HitObject hitObject, HitObject sampledHitObject = null)
{
sampledHitObject ??= hitObject;
if (!sampledHitObject.Samples.Any())
yield break;
// Samples that allow themselves to be overridden by control points have a volume of 0.
int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume);
double samplePlayTime = sampledHitObject.GetEndTime();
EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime);
// We only care about samples played on the edges of objects, not ones like spinnerspin or slidertick.
if (edgeType == EdgeType.None)
yield break;
string postfix = hitObject is IHasDuration ? edgeType.ToString().ToLower() : null;
if (maxVolume <= muted_threshold)
{
if (edgeType == EdgeType.Head)
yield return new IssueTemplateMutedActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
else
yield return new IssueTemplateMutedPassive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
}
else if (maxVolume <= low_volume_threshold && edgeType == EdgeType.Head)
{
yield return new IssueTemplateLowVolumeActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
}
}
private EdgeType getEdgeAtTime(HitObject hitObject, double time)
{
if (Precision.AlmostEquals(time, hitObject.StartTime, 1f))
return EdgeType.Head;
if (Precision.AlmostEquals(time, hitObject.GetEndTime(), 1f))
return EdgeType.Tail;
if (hitObject is IHasRepeats hasRepeats)
{
double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount();
if (spanDuration <= 0)
// Prevents undefined behaviour in cases like where zero/negative-length sliders/hold notes exist.
return EdgeType.None;
double spans = (time - hitObject.StartTime) / spanDuration;
double acceptableDifference = 1 / spanDuration; // 1 ms of acceptable difference, as with head/tail above.
if (Precision.AlmostEquals(spans, Math.Ceiling(spans), acceptableDifference) ||
Precision.AlmostEquals(spans, Math.Floor(spans), acceptableDifference))
{
return EdgeType.Repeat;
}
}
return EdgeType.None;
}
public abstract class IssueTemplateMuted : IssueTemplate
{
protected IssueTemplateMuted(ICheck check, IssueType type, string unformattedMessage)
: base(check, type, unformattedMessage)
{
}
public Issue Create(HitObject hitobject, double volume, double time, string postfix = "")
{
string objectName = hitobject.GetType().Name;
if (!string.IsNullOrEmpty(postfix))
objectName += " " + postfix;
return new Issue(hitobject, this, objectName, volume) { Time = time };
}
}
public class IssueTemplateMutedActive : IssueTemplateMuted
{
public IssueTemplateMutedActive(ICheck check)
: base(check, IssueType.Problem, "{0} has a volume of {1:0%}. Clickable objects must have clearly audible feedback.")
{
}
}
public class IssueTemplateLowVolumeActive : IssueTemplateMuted
{
public IssueTemplateLowVolumeActive(ICheck check)
: base(check, IssueType.Warning, "{0} has a volume of {1:0%}, ensure this is audible.")
{
}
}
public class IssueTemplateMutedPassive : IssueTemplateMuted
{
public IssueTemplateMutedPassive(ICheck check)
: base(check, IssueType.Negligible, "{0} has a volume of {1:0%}, ensure there is no distinct sound here in the song if inaudible.")
{
}
}
}
}

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Bindables;
@ -72,6 +73,9 @@ namespace osu.Game.Scoring
}
}
protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
protected override void ExportModelTo(ScoreInfo model, Stream outputStream)
{
var file = model.Files.SingleOrDefault();

View File

@ -261,7 +261,7 @@ namespace osu.Game.Screens.Menu
switch (state)
{
default:
return true;
return false;
case ButtonSystemState.Initial:
State = ButtonSystemState.TopLevel;

View File

@ -42,6 +42,7 @@ namespace osu.Game.Screens.Select.Carousel
match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate);
match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate);
match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize);
match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(Beatmap.BaseDifficulty.OverallDifficulty);
match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(Beatmap.Length);
match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(Beatmap.BPM);

View File

@ -24,6 +24,7 @@ namespace osu.Game.Screens.Select
public OptionalRange<float> ApproachRate;
public OptionalRange<float> DrainRate;
public OptionalRange<float> CircleSize;
public OptionalRange<float> OverallDifficulty;
public OptionalRange<double> Length;
public OptionalRange<double> BPM;
public OptionalRange<int> BeatDivisor;

View File

@ -51,6 +51,9 @@ namespace osu.Game.Screens.Select
case "cs":
return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value);
case "od":
return TryUpdateCriteriaRange(ref criteria.OverallDifficulty, op, value);
case "bpm":
return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2);

View File

@ -142,16 +142,16 @@ namespace osu.Game.Skinning
return base.ComputeHash(item, reader);
}
protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
{
await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
var instance = GetSkin(model);
model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo();
if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
populateMetadata(model, instance);
return Task.CompletedTask;
}
private void populateMetadata(SkinInfo item, Skin instance)