1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-21 06:05:07 +08:00

Merge pull request #14355 from frenzibyte/mod-settings-difficulty-cache

Update star difficulty bindables on mod settings change
This commit is contained in:
Dean Herbert 2021-08-19 20:00:48 +09:00 committed by GitHub
commit 2b0f2a6db9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 352 additions and 180 deletions

View File

@ -0,0 +1,34 @@
// 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 BenchmarkDotNet.Attributes;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Benchmarks
{
public class BenchmarkMod : BenchmarkTest
{
private OsuModDoubleTime mod;
[Params(1, 10, 100)]
public int Times { get; set; }
[GlobalSetup]
public void GlobalSetup()
{
mod = new OsuModDoubleTime();
}
[Benchmark]
public int ModHashCode()
{
var hashCode = new HashCode();
for (int i = 0; i < Times; i++)
hashCode.Add(mod);
return hashCode.ToHashCode();
}
}
}

View File

@ -1,56 +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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Beatmaps
{
[TestFixture]
public class BeatmapDifficultyCacheTest
{
[Test]
public void TestKeyEqualsWithDifferentModInstances()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
Assert.That(key1, Is.EqualTo(key2));
}
[Test]
public void TestKeyEqualsWithDifferentModOrder()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
Assert.That(key1, Is.EqualTo(key2));
}
[TestCase(1.3, DifficultyRating.Easy)]
[TestCase(1.993, DifficultyRating.Easy)]
[TestCase(1.998, DifficultyRating.Normal)]
[TestCase(2.4, DifficultyRating.Normal)]
[TestCase(2.693, DifficultyRating.Normal)]
[TestCase(2.698, DifficultyRating.Hard)]
[TestCase(3.5, DifficultyRating.Hard)]
[TestCase(3.993, DifficultyRating.Hard)]
[TestCase(3.997, DifficultyRating.Insane)]
[TestCase(5.0, DifficultyRating.Insane)]
[TestCase(5.292, DifficultyRating.Insane)]
[TestCase(5.297, DifficultyRating.Expert)]
[TestCase(6.2, DifficultyRating.Expert)]
[TestCase(6.493, DifficultyRating.Expert)]
[TestCase(6.498, DifficultyRating.ExpertPlus)]
[TestCase(8.3, DifficultyRating.ExpertPlus)]
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
{
var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
Assert.AreEqual(expectedBracket, actualBracket);
}
}
}

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 System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Beatmaps
{
[HeadlessTest]
public class TestSceneBeatmapDifficultyCache : OsuTestScene
{
public const double BASE_STARS = 5.55;
private BeatmapSetInfo importedSet;
[Resolved]
private BeatmapManager beatmaps { get; set; }
private TestBeatmapDifficultyCache difficultyCache;
private IBindable<StarDifficulty?> starDifficultyBindable;
private Queue<ValueChangedEvent<StarDifficulty?>> starDifficultyChangesQueue;
[BackgroundDependencyLoader]
private void load(OsuGameBase osu)
{
importedSet = ImportBeatmapTest.LoadQuickOszIntoOsu(osu).Result;
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("setup difficulty cache", () =>
{
SelectedMods.Value = Array.Empty<Mod>();
Child = difficultyCache = new TestBeatmapDifficultyCache();
starDifficultyChangesQueue = new Queue<ValueChangedEvent<StarDifficulty?>>();
starDifficultyBindable = difficultyCache.GetBindableDifficulty(importedSet.Beatmaps.First());
starDifficultyBindable.BindValueChanged(starDifficultyChangesQueue.Enqueue);
});
AddAssert($"star difficulty -> {BASE_STARS}", () =>
starDifficultyChangesQueue.Dequeue().NewValue?.Stars == BASE_STARS &&
starDifficultyChangesQueue.Count == 0);
}
[Test]
public void TestStarDifficultyChangesOnModSettings()
{
OsuModDoubleTime dt = null;
AddStep("change selected mod to DT", () => SelectedMods.Value = new[] { dt = new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
AddAssert($"star difficulty -> {BASE_STARS + 1.5}", () =>
starDifficultyChangesQueue.Dequeue().NewValue?.Stars == BASE_STARS + 1.5 &&
starDifficultyChangesQueue.Count == 0);
AddStep("change DT speed to 1.25", () => dt.SpeedChange.Value = 1.25);
AddAssert($"star difficulty -> {BASE_STARS + 1.25}", () =>
starDifficultyChangesQueue.Dequeue().NewValue?.Stars == BASE_STARS + 1.25 &&
starDifficultyChangesQueue.Count == 0);
AddStep("change selected mod to NC", () => SelectedMods.Value = new[] { new OsuModNightcore { SpeedChange = { Value = 1.75 } } });
AddAssert($"star difficulty -> {BASE_STARS + 1.75}", () =>
starDifficultyChangesQueue.Dequeue().NewValue?.Stars == BASE_STARS + 1.75 &&
starDifficultyChangesQueue.Count == 0);
}
[Test]
public void TestKeyEqualsWithDifferentModInstances()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
}
[Test]
public void TestKeyEqualsWithDifferentModOrder()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
}
[Test]
public void TestKeyDoesntEqualWithDifferentModSettings()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
Assert.That(key1, Is.Not.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode()));
}
[Test]
public void TestKeyEqualWithMatchingModSettings()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
}
[TestCase(1.3, DifficultyRating.Easy)]
[TestCase(1.993, DifficultyRating.Easy)]
[TestCase(1.998, DifficultyRating.Normal)]
[TestCase(2.4, DifficultyRating.Normal)]
[TestCase(2.693, DifficultyRating.Normal)]
[TestCase(2.698, DifficultyRating.Hard)]
[TestCase(3.5, DifficultyRating.Hard)]
[TestCase(3.993, DifficultyRating.Hard)]
[TestCase(3.997, DifficultyRating.Insane)]
[TestCase(5.0, DifficultyRating.Insane)]
[TestCase(5.292, DifficultyRating.Insane)]
[TestCase(5.297, DifficultyRating.Expert)]
[TestCase(6.2, DifficultyRating.Expert)]
[TestCase(6.493, DifficultyRating.Expert)]
[TestCase(6.498, DifficultyRating.ExpertPlus)]
[TestCase(8.3, DifficultyRating.ExpertPlus)]
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
{
var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
Assert.AreEqual(expectedBracket, actualBracket);
}
private class TestBeatmapDifficultyCache : BeatmapDifficultyCache
{
protected override Task<StarDifficulty> ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default)
{
var rateAdjust = lookup.OrderedMods.OfType<ModRateAdjust>().SingleOrDefault();
if (rateAdjust != null)
return Task.FromResult(new StarDifficulty(BASE_STARS + rateAdjust.SpeedChange.Value, 0));
return Task.FromResult(new StarDifficulty(BASE_STARS, 0));
}
}
}
}

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -65,6 +66,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("show", () => { infoWedge.Show(); }); AddStep("show", () => { infoWedge.Show(); });
AddSliderStep("change star difficulty", 0, 11.9, 5.55, v =>
{
foreach (var hasCurrentValue in infoWedge.Info.ChildrenOfType<IHasCurrentValue<StarDifficulty>>())
hasCurrentValue.Current.Value = new StarDifficulty(v, 0);
});
foreach (var rulesetInfo in rulesets.AvailableRulesets) foreach (var rulesetInfo in rulesets.AvailableRulesets)
{ {
var instance = rulesetInfo.CreateInstance(); var instance = rulesetInfo.CreateInstance();

View File

@ -3,7 +3,6 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
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.Utils; using osu.Framework.Utils;
@ -50,32 +49,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
StarRatingDisplay starRating = null; StarRatingDisplay starRating = null;
BindableDouble starRatingNumeric; AddStep("load display", () => Child = starRating = new StarRatingDisplay(new StarDifficulty(5.55, 1), animated: true)
AddStep("load display", () =>
{
Child = starRating = new StarRatingDisplay(new StarDifficulty(5.55, 1))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(3f),
};
});
AddStep("transform over spectrum", () =>
{
starRatingNumeric = new BindableDouble();
starRatingNumeric.BindValueChanged(val => starRating.Current.Value = new StarDifficulty(val.NewValue, 1));
this.TransformBindableTo(starRatingNumeric, 10, 10000, Easing.OutQuint);
});
}
[Test]
public void TestChangingStarRatingDisplay()
{
StarRatingDisplay starRating = null;
AddStep("load display", () => Child = starRating = new StarRatingDisplay(new StarDifficulty(5.55, 1))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -14,6 +14,7 @@ using osu.Framework.Lists;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -56,12 +57,28 @@ namespace osu.Game.Beatmaps
[Resolved] [Resolved]
private Bindable<IReadOnlyList<Mod>> currentMods { get; set; } private Bindable<IReadOnlyList<Mod>> currentMods { get; set; }
private ModSettingChangeTracker modSettingChangeTracker;
private ScheduledDelegate debouncedModSettingsChange;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
currentRuleset.BindValueChanged(_ => updateTrackedBindables()); currentRuleset.BindValueChanged(_ => updateTrackedBindables());
currentMods.BindValueChanged(_ => updateTrackedBindables(), true);
currentMods.BindValueChanged(mods =>
{
modSettingChangeTracker?.Dispose();
updateTrackedBindables();
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += _ =>
{
debouncedModSettingsChange?.Cancel();
debouncedModSettingsChange = Scheduler.AddDelayed(updateTrackedBindables, 100);
};
}, true);
} }
/// <summary> /// <summary>
@ -84,7 +101,7 @@ namespace osu.Game.Beatmaps
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination. /// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The bindable will not update to follow the currently-selected ruleset and mods. /// The bindable will not update to follow the currently-selected ruleset and mods or its settings.
/// </remarks> /// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param> /// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param> /// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param>
@ -275,6 +292,8 @@ namespace osu.Game.Beatmaps
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
modSettingChangeTracker?.Dispose();
cancelTrackedBindableUpdate(); cancelTrackedBindableUpdate();
updateScheduler?.Dispose(); updateScheduler?.Dispose();
} }
@ -297,7 +316,7 @@ namespace osu.Game.Beatmaps
public bool Equals(DifficultyCacheLookup other) public bool Equals(DifficultyCacheLookup other)
=> Beatmap.ID == other.Beatmap.ID => Beatmap.ID == other.Beatmap.ID
&& Ruleset.ID == other.Ruleset.ID && Ruleset.ID == other.Ruleset.ID
&& OrderedMods.Select(m => m.Acronym).SequenceEqual(other.OrderedMods.Select(m => m.Acronym)); && OrderedMods.SequenceEqual(other.OrderedMods);
public override int GetHashCode() public override int GetHashCode()
{ {
@ -307,7 +326,7 @@ namespace osu.Game.Beatmaps
hashCode.Add(Ruleset.ID); hashCode.Add(Ruleset.ID);
foreach (var mod in OrderedMods) foreach (var mod in OrderedMods)
hashCode.Add(mod.Acronym); hashCode.Add(mod);
return hashCode.ToHashCode(); return hashCode.ToHashCode();
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -22,6 +23,7 @@ namespace osu.Game.Beatmaps.Drawables
/// </summary> /// </summary>
public class StarRatingDisplay : CompositeDrawable, IHasCurrentValue<StarDifficulty> public class StarRatingDisplay : CompositeDrawable, IHasCurrentValue<StarDifficulty>
{ {
private readonly bool animated;
private readonly Box background; private readonly Box background;
private readonly SpriteIcon starIcon; private readonly SpriteIcon starIcon;
private readonly OsuSpriteText starsText; private readonly OsuSpriteText starsText;
@ -34,6 +36,14 @@ namespace osu.Game.Beatmaps.Drawables
set => current.Current = value; set => current.Current = value;
} }
private readonly Bindable<double> displayedStars = new BindableDouble();
/// <summary>
/// The currently displayed stars of this display wrapped in a bindable.
/// This bindable gets transformed on change rather than instantaneous, if animation is enabled.
/// </summary>
public IBindable<double> DisplayedStars => displayedStars;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -45,8 +55,11 @@ namespace osu.Game.Beatmaps.Drawables
/// </summary> /// </summary>
/// <param name="starDifficulty">The already computed <see cref="StarDifficulty"/> to display.</param> /// <param name="starDifficulty">The already computed <see cref="StarDifficulty"/> to display.</param>
/// <param name="size">The size of the star rating display.</param> /// <param name="size">The size of the star rating display.</param>
public StarRatingDisplay(StarDifficulty starDifficulty, StarRatingDisplaySize size = StarRatingDisplaySize.Regular) /// <param name="animated">Whether the star rating display will perform transforms on change rather than updating instantaneously.</param>
public StarRatingDisplay(StarDifficulty starDifficulty, StarRatingDisplaySize size = StarRatingDisplaySize.Regular, bool animated = false)
{ {
this.animated = animated;
Current.Value = starDifficulty; Current.Value = starDifficulty;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -112,7 +125,7 @@ namespace osu.Game.Beatmaps.Drawables
// see https://github.com/ppy/osu-framework/issues/3271. // see https://github.com/ppy/osu-framework/issues/3271.
Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold),
Shadow = false, Shadow = false,
} },
} }
} }
}, },
@ -126,12 +139,22 @@ namespace osu.Game.Beatmaps.Drawables
Current.BindValueChanged(c => Current.BindValueChanged(c =>
{ {
starsText.Text = c.NewValue.Stars.ToString("0.00"); if (animated)
this.TransformBindableTo(displayedStars, c.NewValue.Stars, 750, Easing.OutQuint);
else
displayedStars.Value = c.NewValue.Stars;
});
background.Colour = colours.ForStarDifficulty(c.NewValue.Stars); displayedStars.Value = Current.Value.Stars;
starIcon.Colour = c.NewValue.Stars >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47"); displayedStars.BindValueChanged(s =>
starsText.Colour = c.NewValue.Stars >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f); {
starsText.Text = s.NewValue.ToLocalisableString("0.00");
background.Colour = colours.ForStarDifficulty(s.NewValue);
starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47");
starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f);
}, true); }, true);
} }
} }

View File

@ -129,6 +129,17 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore] [JsonIgnore]
public virtual Type[] IncompatibleMods => Array.Empty<Type>(); public virtual Type[] IncompatibleMods => Array.Empty<Type>();
private IReadOnlyList<IBindable> settingsBacking;
/// <summary>
/// A list of the all <see cref="IBindable"/> settings within this mod.
/// </summary>
internal IReadOnlyList<IBindable> Settings =>
settingsBacking ??= this.GetSettingsSourceProperties()
.Select(p => p.Item2.GetValue(this))
.Cast<IBindable>()
.ToList();
/// <summary> /// <summary>
/// Creates a copy of this <see cref="Mod"/> initialised to a default state. /// Creates a copy of this <see cref="Mod"/> initialised to a default state.
/// </summary> /// </summary>
@ -191,15 +202,39 @@ namespace osu.Game.Rulesets.Mods
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;
return GetType() == other.GetType() && return GetType() == other.GetType() &&
this.GetSettingsSourceProperties().All(pair => Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
EqualityComparer<object>.Default.Equals( }
ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(this)),
ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(other)))); public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(GetType());
foreach (var setting in Settings)
hashCode.Add(ModUtils.GetSettingUnderlyingValue(setting));
return hashCode.ToHashCode();
} }
/// <summary> /// <summary>
/// Reset all custom settings for this mod back to their defaults. /// Reset all custom settings for this mod back to their defaults.
/// </summary> /// </summary>
public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType())); public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType()));
private class ModSettingsEqualityComparer : IEqualityComparer<IBindable>
{
public static ModSettingsEqualityComparer Default { get; } = new ModSettingsEqualityComparer();
public bool Equals(IBindable x, IBindable y)
{
object xValue = x == null ? null : ModUtils.GetSettingUnderlyingValue(x);
object yValue = y == null ? null : ModUtils.GetSettingUnderlyingValue(y);
return EqualityComparer<object>.Default.Equals(xValue, yValue);
}
public int GetHashCode(IBindable obj) => ModUtils.GetSettingUnderlyingValue(obj).GetHashCode();
}
} }
} }

View File

@ -37,6 +37,8 @@ namespace osu.Game.Screens.Select
public const float BORDER_THICKNESS = 2.5f; public const float BORDER_THICKNESS = 2.5f;
private const float shear_width = 36.75f; private const float shear_width = 36.75f;
private const float transition_duration = 250;
private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0); private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0);
[Resolved] [Resolved]
@ -45,11 +47,6 @@ namespace osu.Game.Screens.Select
[Resolved] [Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; } private IBindable<IReadOnlyList<Mod>> mods { get; set; }
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
private IBindable<StarDifficulty?> beatmapDifficulty;
protected Container DisplayedContent { get; private set; } protected Container DisplayedContent { get; private set; }
protected WedgeInfoText Info { get; private set; } protected WedgeInfoText Info { get; private set; }
@ -76,24 +73,24 @@ namespace osu.Game.Screens.Select
ruleset.BindValueChanged(_ => updateDisplay()); ruleset.BindValueChanged(_ => updateDisplay());
} }
private const double animation_duration = 800;
protected override void PopIn() protected override void PopIn()
{ {
this.MoveToX(0, 800, Easing.OutQuint); this.MoveToX(0, animation_duration, Easing.OutQuint);
this.RotateTo(0, 800, Easing.OutQuint); this.RotateTo(0, animation_duration, Easing.OutQuint);
this.FadeIn(250); this.FadeIn(transition_duration);
} }
protected override void PopOut() protected override void PopOut()
{ {
this.MoveToX(-100, 800, Easing.In); this.MoveToX(-100, animation_duration, Easing.In);
this.RotateTo(10, 800, Easing.In); this.RotateTo(10, animation_duration, Easing.In);
this.FadeOut(500, Easing.In); this.FadeOut(transition_duration * 2, Easing.In);
} }
private WorkingBeatmap beatmap; private WorkingBeatmap beatmap;
private CancellationTokenSource cancellationSource;
public WorkingBeatmap Beatmap public WorkingBeatmap Beatmap
{ {
get => beatmap; get => beatmap;
@ -102,12 +99,6 @@ namespace osu.Game.Screens.Select
if (beatmap == value) return; if (beatmap == value) return;
beatmap = value; beatmap = value;
cancellationSource?.Cancel();
cancellationSource = new CancellationTokenSource();
beatmapDifficulty?.UnbindAll();
beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, cancellationSource.Token);
beatmapDifficulty.BindValueChanged(_ => updateDisplay());
updateDisplay(); updateDisplay();
} }
@ -127,7 +118,7 @@ namespace osu.Game.Screens.Select
{ {
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
DisplayedContent?.FadeOut(250); DisplayedContent?.FadeOut(transition_duration);
DisplayedContent?.Expire(); DisplayedContent?.Expire();
DisplayedContent = null; DisplayedContent = null;
} }
@ -146,7 +137,7 @@ namespace osu.Game.Screens.Select
Children = new Drawable[] Children = new Drawable[]
{ {
new BeatmapInfoWedgeBackground(beatmap), new BeatmapInfoWedgeBackground(beatmap),
Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty()), Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value),
} }
}, loaded => }, loaded =>
{ {
@ -159,12 +150,6 @@ namespace osu.Game.Screens.Select
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
cancellationSource?.Cancel();
}
public class WedgeInfoText : Container public class WedgeInfoText : Container
{ {
public OsuSpriteText VersionLabel { get; private set; } public OsuSpriteText VersionLabel { get; private set; }
@ -173,6 +158,9 @@ namespace osu.Game.Screens.Select
public BeatmapSetOnlineStatusPill StatusPill { get; private set; } public BeatmapSetOnlineStatusPill StatusPill { get; private set; }
public FillFlowContainer MapperContainer { get; private set; } public FillFlowContainer MapperContainer { get; private set; }
private Container difficultyColourBar;
private StarRatingDisplay starRatingDisplay;
private ILocalisedBindableString titleBinding; private ILocalisedBindableString titleBinding;
private ILocalisedBindableString artistBinding; private ILocalisedBindableString artistBinding;
private FillFlowContainer infoLabelContainer; private FillFlowContainer infoLabelContainer;
@ -181,20 +169,21 @@ namespace osu.Game.Screens.Select
private readonly WorkingBeatmap beatmap; private readonly WorkingBeatmap beatmap;
private readonly RulesetInfo ruleset; private readonly RulesetInfo ruleset;
private readonly IReadOnlyList<Mod> mods; private readonly IReadOnlyList<Mod> mods;
private readonly StarDifficulty starDifficulty;
private ModSettingChangeTracker settingChangeTracker; private ModSettingChangeTracker settingChangeTracker;
public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty) public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
this.mods = mods; this.mods = mods;
starDifficulty = difficulty;
} }
private CancellationTokenSource cancellationSource;
private IBindable<StarDifficulty?> starDifficulty;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(LocalisationManager localisation) private void load(OsuColour colours, LocalisationManager localisation, BeatmapDifficultyCache difficultyCache)
{ {
var beatmapInfo = beatmap.BeatmapInfo; var beatmapInfo = beatmap.BeatmapInfo;
var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
@ -204,12 +193,30 @@ namespace osu.Game.Screens.Select
titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title));
artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist)); artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist));
const float top_height = 0.7f;
Children = new Drawable[] Children = new Drawable[]
{ {
new DifficultyColourBar(starDifficulty) difficultyColourBar = new Container
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = 20, Width = 20f,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Width = top_height,
},
new Box
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Alpha = 0.5f,
X = top_height,
Width = 1 - top_height,
}
}
}, },
new FillFlowContainer new FillFlowContainer
{ {
@ -240,14 +247,16 @@ namespace osu.Game.Screens.Select
Padding = new MarginPadding { Top = 14, Right = shear_width / 2 }, Padding = new MarginPadding { Top = 14, Right = shear_width / 2 },
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Shear = wedged_container_shear, Shear = wedged_container_shear,
Children = new[] Spacing = new Vector2(0f, 5f),
Children = new Drawable[]
{ {
createStarRatingDisplay(starDifficulty).With(display => starRatingDisplay = new StarRatingDisplay(default, animated: true)
{ {
display.Anchor = Anchor.TopRight; Anchor = Anchor.TopRight,
display.Origin = Anchor.TopRight; Origin = Anchor.TopRight,
display.Shear = -wedged_container_shear; Shear = -wedged_container_shear,
}), Alpha = 0f,
},
StatusPill = new BeatmapSetOnlineStatusPill StatusPill = new BeatmapSetOnlineStatusPill
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
@ -303,6 +312,18 @@ namespace osu.Game.Screens.Select
titleBinding.BindValueChanged(_ => setMetadata(metadata.Source)); titleBinding.BindValueChanged(_ => setMetadata(metadata.Source));
artistBinding.BindValueChanged(_ => setMetadata(metadata.Source), true); artistBinding.BindValueChanged(_ => setMetadata(metadata.Source), true);
starRatingDisplay.DisplayedStars.BindValueChanged(s =>
{
difficultyColourBar.Colour = colours.ForStarDifficulty(s.NewValue);
}, true);
starDifficulty = difficultyCache.GetBindableDifficulty(beatmapInfo, (cancellationSource = new CancellationTokenSource()).Token);
starDifficulty.BindValueChanged(s =>
{
starRatingDisplay.FadeIn(transition_duration);
starRatingDisplay.Current.Value = s.NewValue ?? default;
});
// no difficulty means it can't have a status to show // no difficulty means it can't have a status to show
if (beatmapInfo.Version == null) if (beatmapInfo.Version == null)
StatusPill.Hide(); StatusPill.Hide();
@ -310,13 +331,6 @@ namespace osu.Game.Screens.Select
addInfoLabels(); addInfoLabels();
} }
private static Drawable createStarRatingDisplay(StarDifficulty difficulty) => difficulty.Stars > 0
? new StarRatingDisplay(difficulty)
{
Margin = new MarginPadding { Bottom = 5 }
}
: Empty();
private void setMetadata(string source) private void setMetadata(string source)
{ {
ArtistLabel.Text = artistBinding.Value; ArtistLabel.Text = artistBinding.Value;
@ -428,6 +442,7 @@ namespace osu.Game.Screens.Select
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
settingChangeTracker?.Dispose(); settingChangeTracker?.Dispose();
cancellationSource?.Cancel();
} }
public class InfoLabel : Container, IHasTooltip public class InfoLabel : Container, IHasTooltip
@ -488,43 +503,6 @@ namespace osu.Game.Screens.Select
}; };
} }
} }
private class DifficultyColourBar : Container
{
private readonly StarDifficulty difficulty;
public DifficultyColourBar(StarDifficulty difficulty)
{
this.difficulty = difficulty;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
const float full_opacity_ratio = 0.7f;
var difficultyColour = colours.ForStarDifficulty(difficulty.Stars);
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = difficultyColour,
Width = full_opacity_ratio,
},
new Box
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Colour = difficultyColour,
Alpha = 0.5f,
X = full_opacity_ratio,
Width = 1 - full_opacity_ratio,
}
};
}
}
} }
} }
} }