diff --git a/osu.Android.props b/osu.Android.props
index 196d122a2a..c78dfb6a55 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
+
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index d06c4b6746..5fb09c0cef 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -69,7 +69,6 @@ namespace osu.Desktop
/// Allow a maximum of one unhandled exception, per second of execution.
///
///
- ///
private static bool handleException(Exception arg)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index c81710ed18..26e5d381e2 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -482,7 +482,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Retrieves the sample info list at a point in time.
///
/// The time to retrieve the sample info list from.
- ///
private IList sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
///
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
index 856b6554b9..0ba775e5c7 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private void runSpmTest(Mod mod)
{
- SpinnerSpmCounter spmCounter = null;
+ SpinnerSpmCalculator spmCalculator = null;
CreateModTest(new ModTestData
{
@@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
});
- AddUntilStep("fetch SPM counter", () =>
+ AddUntilStep("fetch SPM calculator", () =>
{
- spmCounter = this.ChildrenOfType().SingleOrDefault();
- return spmCounter != null;
+ spmCalculator = this.ChildrenOfType().SingleOrDefault();
+ return spmCalculator != null;
});
- AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
+ AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5));
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
index 7df5ca0f7c..24e69703a6 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
@@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Beatmap = singleSpinnerBeatmap,
PassCondition = () =>
{
- var counter = Player.ChildrenOfType().SingleOrDefault();
- return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1);
+ var counter = Player.ChildrenOfType().SingleOrDefault();
+ return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1);
}
});
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png
new file mode 100644
index 0000000000..73753554f7
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index ac8d5c81bc..14c709cae1 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -168,13 +168,13 @@ namespace osu.Game.Rulesets.Osu.Tests
double estimatedSpm = 0;
addSeekStep(1000);
- AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
+ AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
addSeekStep(2000);
- AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
+ AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
addSeekStep(1000);
- AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
+ AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
}
[TestCase(0.5)]
@@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("retrieve spinner state", () =>
{
expectedProgress = drawableSpinner.Progress;
- expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
+ expectedSpm = drawableSpinner.SpinsPerMinute.Value;
});
addSeekStep(0);
@@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
- AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
+ AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
}
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 39e78a14aa..3a4753761a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
public SpinnerRotationTracker RotationTracker { get; private set; }
- public SpinnerSpmCounter SpmCounter { get; private set; }
+
+ private SpinnerSpmCalculator spmCalculator;
private Container ticks;
private PausableSkinnableSound spinningSample;
@@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
public IBindable GainedBonus => gainedBonus;
- private readonly Bindable gainedBonus = new Bindable();
+ private readonly Bindable gainedBonus = new BindableDouble();
+
+ ///
+ /// The number of spins per minute this spinner is spinning at, for display purposes.
+ ///
+ public readonly IBindable SpinsPerMinute = new BindableDouble();
private const double fade_out_duration = 160;
@@ -63,8 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
- InternalChildren = new Drawable[]
+ AddRangeInternal(new Drawable[]
{
+ spmCalculator = new SpinnerSpmCalculator
+ {
+ Result = { BindTarget = SpinsPerMinute },
+ },
ticks = new Container(),
new AspectContainer
{
@@ -77,20 +87,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RotationTracker = new SpinnerRotationTracker(this)
}
},
- SpmCounter = new SpinnerSpmCounter
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Y = 120,
- Alpha = 0
- },
spinningSample = new PausableSkinnableSound
{
Volume = { Value = 0 },
Looping = true,
Frequency = { Value = spinning_sample_initial_frequency }
}
- };
+ });
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
}
@@ -161,17 +164,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
- protected override void UpdateStartTimeStateTransforms()
- {
- base.UpdateStartTimeStateTransforms();
-
- if (Result?.TimeStarted is double startTime)
- {
- using (BeginAbsoluteSequence(startTime))
- fadeInCounter();
- }
- }
-
protected override void UpdateHitStateTransforms(ArmedState state)
{
base.UpdateHitStateTransforms(state);
@@ -282,22 +274,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateAfterChildren();
- if (!SpmCounter.IsPresent && RotationTracker.Tracking)
- {
- Result.TimeStarted ??= Time.Current;
- fadeInCounter();
- }
+ if (Result.TimeStarted == null && RotationTracker.Tracking)
+ Result.TimeStarted = Time.Current;
// don't update after end time to avoid the rate display dropping during fade out.
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
if (Time.Current <= HitObject.EndTime)
- SpmCounter.SetRotation(Result.RateAdjustedRotation);
+ spmCalculator.SetRotation(Result.RateAdjustedRotation);
updateBonusScore();
}
- private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
-
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
private int wholeSpins;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
index 891821fe2f..ae8c03dad1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
@@ -1,6 +1,7 @@
// 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.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private OsuSpriteText bonusCounter;
+ private Container spmContainer;
+ private OsuSpriteText spmCounter;
+
public DefaultSpinner()
{
RelativeSizeAxes = Axes.Both;
@@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 24),
Y = -120,
+ },
+ spmContainer = new Container
+ {
+ Alpha = 0f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = 120,
+ Children = new[]
+ {
+ spmCounter = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"0",
+ Font = OsuFont.Numeric.With(size: 24)
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"SPINS PER MINUTE",
+ Font = OsuFont.Numeric.With(size: 12),
+ Y = 30
+ }
+ }
}
});
}
private IBindable gainedBonus;
+ private IBindable spinsPerMinute;
protected override void LoadComplete()
{
@@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
});
+
+ spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
+ spinsPerMinute.BindValueChanged(spm =>
+ {
+ spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
+ }, true);
+
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
+ fadeCounterOnTimeStart();
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ {
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
+ fadeCounterOnTimeStart();
+ }
+
+ private void fadeCounterOnTimeStart()
+ {
+ if (drawableSpinner.Result?.TimeStarted is double startTime)
+ {
+ using (BeginAbsoluteSequence(startTime))
+ spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
similarity index 61%
rename from osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
index 69355f624b..a5205bbb8c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
@@ -1,77 +1,37 @@
// 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.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
- public class SpinnerSpmCounter : Container
+ public class SpinnerSpmCalculator : Component
{
+ private readonly Queue records = new Queue();
+ private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
+
+ ///
+ /// The resultant spins per minute value, which is updated via .
+ ///
+ public IBindable Result => result;
+
+ private readonly Bindable result = new BindableDouble();
+
[Resolved]
private DrawableHitObject drawableSpinner { get; set; }
- private readonly OsuSpriteText spmText;
-
- public SpinnerSpmCounter()
- {
- Children = new Drawable[]
- {
- spmText = new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = @"0",
- Font = OsuFont.Numeric.With(size: 24)
- },
- new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = @"SPINS PER MINUTE",
- Font = OsuFont.Numeric.With(size: 12),
- Y = 30
- }
- };
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
drawableSpinner.HitObjectApplied += resetState;
}
- private double spm;
-
- public double SpinsPerMinute
- {
- get => spm;
- private set
- {
- if (value == spm) return;
-
- spm = value;
- spmText.Text = Math.Truncate(value).ToString(@"#0");
- }
- }
-
- private struct RotationRecord
- {
- public float Rotation;
- public double Time;
- }
-
- private readonly Queue records = new Queue();
- private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
-
public void SetRotation(float currentRotation)
{
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
@@ -88,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
record = records.Dequeue();
- SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
+ result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
}
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
@@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private void resetState(DrawableHitObject hitObject)
{
- SpinsPerMinute = 0;
+ result.Value = 0;
records.Clear();
}
@@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
if (drawableSpinner != null)
drawableSpinner.HitObjectApplied -= resetState;
}
+
+ private struct RotationRecord
+ {
+ public float Rotation;
+ public double Time;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 064b7a4680..7eb6898abc 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected const float SPRITE_SCALE = 0.625f;
+ private const float spm_hide_offset = 50f;
+
protected DrawableSpinner DrawableSpinner { get; private set; }
private Sprite spin;
@@ -35,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private LegacySpriteText bonusCounter;
+ private Sprite spmBackground;
+ private LegacySpriteText spmCounter;
+
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
{
@@ -79,11 +84,27 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 299,
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
+ spmBackground = new Sprite
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopLeft,
+ Texture = source.GetTexture("spinner-rpm"),
+ Scale = new Vector2(SPRITE_SCALE),
+ Position = new Vector2(-87, 445 + spm_hide_offset),
+ },
+ spmCounter = new LegacySpriteText(source, LegacyFont.Score)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopRight,
+ Scale = new Vector2(SPRITE_SCALE * 0.9f),
+ Position = new Vector2(80, 448 + spm_hide_offset),
+ }.With(s => s.Font = s.Font.With(fixedWidth: false)),
}
});
}
private IBindable gainedBonus;
+ private IBindable spinsPerMinute;
private readonly Bindable completed = new Bindable();
@@ -99,6 +120,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
});
+ spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy();
+ spinsPerMinute.BindValueChanged(spm =>
+ {
+ spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
+ }, true);
+
completed.BindValueChanged(onCompletedChanged, true);
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
@@ -142,10 +169,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (drawableHitObject)
{
case DrawableSpinner d:
- double fadeOutLength = Math.Min(400, d.HitObject.Duration);
+ using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn))
+ {
+ spmBackground.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
+ spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
+ }
- using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true))
- spin.FadeOutFromOne(fadeOutLength);
+ double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
+
+ using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
+ spin.FadeOutFromOne(spinFadeOutLength);
break;
case DrawableSpinnerTick d:
diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
new file mode 100644
index 0000000000..7a5789f01a
--- /dev/null
+++ b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Online.API;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Tests.Mods
+{
+ [TestFixture]
+ public class ModSettingsEqualityComparison
+ {
+ [Test]
+ public void Test()
+ {
+ var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } };
+ var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
+ var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
+ var apiMod1 = new APIMod(mod1);
+ var apiMod2 = new APIMod(mod2);
+ var apiMod3 = new APIMod(mod3);
+
+ Assert.That(mod1, Is.Not.EqualTo(mod2));
+ Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
+
+ Assert.That(mod2, Is.EqualTo(mod2));
+ Assert.That(apiMod2, Is.EqualTo(apiMod2));
+
+ Assert.That(mod2, Is.EqualTo(mod3));
+ Assert.That(apiMod2, Is.EqualTo(apiMod3));
+
+ Assert.That(mod3, Is.EqualTo(mod2));
+ Assert.That(apiMod3, Is.EqualTo(apiMod2));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index 77f910c144..3afb7481b1 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -11,7 +11,10 @@ using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
namespace osu.Game.Tests.Online
{
@@ -84,6 +87,36 @@ namespace osu.Game.Tests.Online
Assert.That(converted?.OverallDifficulty.Value, Is.EqualTo(11));
}
+ [Test]
+ public void TestDeserialiseScoreInfoWithEmptyMods()
+ {
+ var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo };
+
+ var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score));
+
+ if (deserialised != null)
+ deserialised.Ruleset = new OsuRuleset().RulesetInfo;
+
+ Assert.That(deserialised?.Mods.Length, Is.Zero);
+ }
+
+ [Test]
+ public void TestDeserialiseScoreInfoWithCustomModSetting()
+ {
+ var score = new ScoreInfo
+ {
+ Ruleset = new OsuRuleset().RulesetInfo,
+ Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } }
+ };
+
+ var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score));
+
+ if (deserialised != null)
+ deserialised.Ruleset = new OsuRuleset().RulesetInfo;
+
+ Assert.That(((OsuModDoubleTime)deserialised?.Mods[0])?.SpeedChange.Value, Is.EqualTo(2));
+ }
+
private class TestRuleset : Ruleset
{
public override IEnumerable GetModsFor(ModType type) => new Mod[]
diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
index 1da6433707..88b4614791 100644
--- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
+++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
@@ -64,6 +64,13 @@ namespace osu.Game.Tests.Visual.Editing
});
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Clock.Seek(2500);
+ }
+
public abstract Drawable CreateTestComponent();
private class AudioVisualiser : CompositeDrawable
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index 1ee848b902..b6c06bb149 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Online;
using osu.Game.Online.API;
@@ -38,6 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
+ private OsuConfigManager config;
+
public TestSceneMultiplayerGameplayLeaderboard()
{
base.Content.Children = new Drawable[]
@@ -48,6 +51,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
+ }
+
[SetUpSteps]
public override void SetUpSteps()
{
@@ -97,6 +106,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users);
}
+ [Test]
+ public void TestChangeScoringMode()
+ {
+ AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 5);
+ AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
+ AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
+ }
+
public class TestMultiplayerStreaming : SpectatorStreamingClient
{
public new BindableList PlayingUsers => (BindableList)base.PlayingUsers;
@@ -163,7 +180,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
break;
}
- ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty()));
+ ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) }));
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index 839118de2f..caa731f985 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
- AddAssert("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
+ AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs
new file mode 100644
index 0000000000..3b2cfb1c7b
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs
@@ -0,0 +1,229 @@
+// Copyright (c) ppy Pty Ltd . 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 System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Framework.Utils;
+using osu.Game.Database;
+using osu.Game.Online;
+using osu.Game.Online.Spectator;
+using osu.Game.Replays.Legacy;
+using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerSpectatorLeaderboard : MultiplayerTestScene
+ {
+ [Cached(typeof(SpectatorStreamingClient))]
+ private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
+
+ [Cached(typeof(UserLookupCache))]
+ private UserLookupCache lookupCache = new TestUserLookupCache();
+
+ protected override Container Content => content;
+ private readonly Container content;
+
+ private readonly Dictionary clocks = new Dictionary
+ {
+ { 55, new ManualClock() },
+ { 56, new ManualClock() }
+ };
+
+ public TestSceneMultiplayerSpectatorLeaderboard()
+ {
+ base.Content.AddRange(new Drawable[]
+ {
+ streamingClient,
+ lookupCache,
+ content = new Container { RelativeSizeAxes = Axes.Both }
+ });
+ }
+
+ [SetUpSteps]
+ public new void SetUpSteps()
+ {
+ MultiplayerSpectatorLeaderboard leaderboard = null;
+
+ AddStep("reset", () =>
+ {
+ Clear();
+
+ foreach (var (userId, clock) in clocks)
+ {
+ streamingClient.EndPlay(userId, 0);
+ clock.CurrentTime = 0;
+ }
+ });
+
+ AddStep("create leaderboard", () =>
+ {
+ foreach (var (userId, _) in clocks)
+ streamingClient.StartPlay(userId, 0);
+
+ Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
+
+ var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
+ var scoreProcessor = new OsuScoreProcessor();
+ scoreProcessor.ApplyBeatmap(playable);
+
+ LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add);
+ });
+
+ AddUntilStep("wait for load", () => leaderboard.IsLoaded);
+
+ AddStep("add clock sources", () =>
+ {
+ foreach (var (userId, clock) in clocks)
+ leaderboard.AddClock(userId, clock);
+ });
+ }
+
+ [Test]
+ public void TestLeaderboardTracksCurrentTime()
+ {
+ AddStep("send frames", () =>
+ {
+ // For user 55, send frames in sets of 1.
+ // For user 56, send frames in sets of 10.
+ for (int i = 0; i < 100; i++)
+ {
+ streamingClient.SendFrames(55, i, 1);
+
+ if (i % 10 == 0)
+ streamingClient.SendFrames(56, i, 10);
+ }
+ });
+
+ assertCombo(55, 1);
+ assertCombo(56, 10);
+
+ // Advance to a point where only user 55's frame changes.
+ setTime(500);
+ assertCombo(55, 5);
+ assertCombo(56, 10);
+
+ // Advance to a point where both user's frame changes.
+ setTime(1100);
+ assertCombo(55, 11);
+ assertCombo(56, 20);
+
+ // Advance user 56 only to a point where its frame changes.
+ setTime(56, 2100);
+ assertCombo(55, 11);
+ assertCombo(56, 30);
+
+ // Advance both users beyond their last frame
+ setTime(101 * 100);
+ assertCombo(55, 100);
+ assertCombo(56, 100);
+ }
+
+ [Test]
+ public void TestNoFrames()
+ {
+ assertCombo(55, 0);
+ assertCombo(56, 0);
+ }
+
+ private void setTime(double time) => AddStep($"set time {time}", () =>
+ {
+ foreach (var (_, clock) in clocks)
+ clock.CurrentTime = time;
+ });
+
+ private void setTime(int userId, double time)
+ => AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
+
+ private void assertCombo(int userId, int expectedCombo)
+ => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
+
+ private class TestSpectatorStreamingClient : SpectatorStreamingClient
+ {
+ private readonly Dictionary userBeatmapDictionary = new Dictionary();
+ private readonly Dictionary userSentStateDictionary = new Dictionary();
+
+ public TestSpectatorStreamingClient()
+ : base(new DevelopmentEndpointConfiguration())
+ {
+ }
+
+ public void StartPlay(int userId, int beatmapId)
+ {
+ userBeatmapDictionary[userId] = beatmapId;
+ userSentStateDictionary[userId] = false;
+ sendState(userId, beatmapId);
+ }
+
+ public void EndPlay(int userId, int beatmapId)
+ {
+ ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
+ {
+ BeatmapID = beatmapId,
+ RulesetID = 0,
+ });
+ userSentStateDictionary[userId] = false;
+ }
+
+ public void SendFrames(int userId, int index, int count)
+ {
+ var frames = new List();
+
+ for (int i = index; i < index + count; i++)
+ {
+ var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
+ frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
+ }
+
+ var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
+ ((ISpectatorClient)this).UserSentFrames(userId, bundle);
+ if (!userSentStateDictionary[userId])
+ sendState(userId, userBeatmapDictionary[userId]);
+ }
+
+ public override void WatchUser(int userId)
+ {
+ if (userSentStateDictionary[userId])
+ {
+ // usually the server would do this.
+ sendState(userId, userBeatmapDictionary[userId]);
+ }
+
+ base.WatchUser(userId);
+ }
+
+ private void sendState(int userId, int beatmapId)
+ {
+ ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState
+ {
+ BeatmapID = beatmapId,
+ RulesetID = 0,
+ });
+ userSentStateDictionary[userId] = true;
+ }
+ }
+
+ private class TestUserLookupCache : UserLookupCache
+ {
+ protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
+ {
+ return Task.FromResult(new User
+ {
+ Id = lookup,
+ Username = $"User {lookup}"
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs
new file mode 100644
index 0000000000..c0958c7fe8
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs
@@ -0,0 +1,115 @@
+// 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.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
+using osuTK.Graphics;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerSpectatorPlayerGrid : OsuManualInputManagerTestScene
+ {
+ private PlayerGrid grid;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Child = grid = new PlayerGrid { RelativeSizeAxes = Axes.Both };
+ });
+
+ [Test]
+ public void TestMaximiseAndMinimise()
+ {
+ addCells(2);
+
+ assertMaximisation(0, false, true);
+ assertMaximisation(1, false, true);
+
+ clickCell(0);
+ assertMaximisation(0, true);
+ assertMaximisation(1, false, true);
+ clickCell(0);
+ assertMaximisation(0, false);
+ assertMaximisation(1, false, true);
+
+ clickCell(1);
+ assertMaximisation(1, true);
+ assertMaximisation(0, false, true);
+ clickCell(1);
+ assertMaximisation(1, false);
+ assertMaximisation(0, false, true);
+ }
+
+ [Test]
+ public void TestClickBothCellsSimultaneously()
+ {
+ addCells(2);
+
+ AddStep("click cell 0 then 1", () =>
+ {
+ InputManager.MoveMouseTo(grid.Content.ElementAt(0));
+ InputManager.Click(MouseButton.Left);
+
+ InputManager.MoveMouseTo(grid.Content.ElementAt(1));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ assertMaximisation(1, true);
+ assertMaximisation(0, false);
+ }
+
+ [TestCase(1)]
+ [TestCase(2)]
+ [TestCase(3)]
+ [TestCase(4)]
+ [TestCase(5)]
+ [TestCase(9)]
+ [TestCase(11)]
+ [TestCase(12)]
+ [TestCase(15)]
+ [TestCase(16)]
+ public void TestCellCount(int count)
+ {
+ addCells(count);
+ AddWaitStep("wait for display", 2);
+ }
+
+ private void addCells(int count) => AddStep($"add {count} grid cells", () =>
+ {
+ for (int i = 0; i < count; i++)
+ grid.Add(new GridContent());
+ });
+
+ private void clickCell(int index) => AddStep($"click cell index {index}", () =>
+ {
+ InputManager.MoveMouseTo(grid.Content.ElementAt(index));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ private void assertMaximisation(int index, bool shouldBeMaximised, bool instant = false)
+ {
+ string assertionText = $"cell index {index} {(shouldBeMaximised ? "is" : "is not")} maximised";
+
+ if (instant)
+ AddAssert(assertionText, checkAction);
+ else
+ AddUntilStep(assertionText, checkAction);
+
+ bool checkAction() => Precision.AlmostEquals(grid.MaximisedFacade.DrawSize, grid.Content.ElementAt(index).DrawSize, 10) == shouldBeMaximised;
+ }
+
+ private class GridContent : Box
+ {
+ public GridContent()
+ {
+ RelativeSizeAxes = Axes.Both;
+ Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
index 57ce4c41e7..484c59695e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
@@ -19,13 +19,12 @@ namespace osu.Game.Tests.Visual.Online
{
UserHistoryGraph graph;
- Add(graph = new UserHistoryGraph
+ Add(graph = new UserHistoryGraph("Test")
{
RelativeSizeAxes = Axes.X,
Height = 200,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- TooltipCounterName = "Test"
});
var values = new[]
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index 2ee52c35aa..92eb7ac713 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -141,7 +141,6 @@ namespace osu.Game.Tournament
///
/// Add missing player info based on user IDs.
///
- ///
private bool addPlayers()
{
bool addedInfo = false;
diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs
index 9847ea020a..769b33009a 100644
--- a/osu.Game/Beatmaps/IBeatmap.cs
+++ b/osu.Game/Beatmaps/IBeatmap.cs
@@ -44,7 +44,6 @@ namespace osu.Game.Beatmaps
///
/// Returns statistics for the contained in this beatmap.
///
- ///
IEnumerable GetStatistics();
///
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 387cfbb193..f9b1c9618b 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -3,7 +3,6 @@
using System;
using System.Diagnostics;
-using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions;
@@ -143,7 +142,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
- SetDefault(OsuSetting.EditorWaveformOpacity, 1f);
+ SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f);
}
public OsuConfigManager(Storage storage)
@@ -169,14 +168,9 @@ namespace osu.Game.Configuration
int combined = (year * 10000) + monthDay;
- if (combined < 20200305)
+ if (combined < 20210413)
{
- // the maximum value of this setting was changed.
- // if we don't manually increase this, it causes song select to filter out beatmaps the user expects to see.
- var maxStars = (BindableDouble)GetOriginalBindable(OsuSetting.DisplayStarsMaximum);
-
- if (maxStars.Value == 10)
- maxStars.Value = maxStars.MaxValue;
+ SetValue(OsuSetting.EditorWaveformOpacity, 0.25f);
}
}
diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs
index f8c9bdeaf8..86e84b0732 100644
--- a/osu.Game/Configuration/SettingsStore.cs
+++ b/osu.Game/Configuration/SettingsStore.cs
@@ -22,7 +22,6 @@ namespace osu.Game.Configuration
///
/// The ruleset's internal ID.
/// An optional variant.
- ///
public List Query(int? rulesetId = null, int? variant = null) =>
ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs
index 0e9382279a..67cee883c8 100644
--- a/osu.Game/Graphics/Backgrounds/Triangles.cs
+++ b/osu.Game/Graphics/Backgrounds/Triangles.cs
@@ -346,7 +346,6 @@ namespace osu.Game.Graphics.Backgrounds
/// such that the smaller triangles appear on top.
///
///
- ///
public int CompareTo(TriangleParticle other) => other.Scale.CompareTo(Scale);
}
}
diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs
index ba188963ea..c8d5ce39a6 100644
--- a/osu.Game/IO/Serialization/IJsonSerializable.cs
+++ b/osu.Game/IO/Serialization/IJsonSerializable.cs
@@ -22,7 +22,6 @@ namespace osu.Game.IO.Serialization
///
/// Creates the default that should be used for all s.
///
- ///
public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs
index b25b00eb84..9d0cfedc03 100644
--- a/osu.Game/Input/KeyBindingStore.cs
+++ b/osu.Game/Input/KeyBindingStore.cs
@@ -85,7 +85,6 @@ namespace osu.Game.Input
///
/// The ruleset's internal ID.
/// An optional variant.
- ///
public List Query(int? rulesetId = null, int? variant = null) =>
ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs
index bff08b0515..4427c82a8b 100644
--- a/osu.Game/Online/API/APIMod.cs
+++ b/osu.Game/Online/API/APIMod.cs
@@ -11,11 +11,12 @@ using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
+using osu.Game.Utils;
namespace osu.Game.Online.API
{
[MessagePackObject]
- public class APIMod : IMod
+ public class APIMod : IMod, IEquatable
{
[JsonProperty("acronym")]
[Key(0)]
@@ -63,7 +64,16 @@ namespace osu.Game.Online.API
return resultMod;
}
- public bool Equals(IMod other) => Acronym == other?.Acronym;
+ public bool Equals(IMod other) => other is APIMod them && Equals(them);
+
+ public bool Equals(APIMod other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+
+ return Acronym == other.Acronym &&
+ Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
+ }
public override string ToString()
{
@@ -72,5 +82,20 @@ namespace osu.Game.Online.API
return $"{Acronym}";
}
+
+ private class ModSettingsEqualityComparer : IEqualityComparer>
+ {
+ public static ModSettingsEqualityComparer Default { get; } = new ModSettingsEqualityComparer();
+
+ public bool Equals(KeyValuePair x, KeyValuePair y)
+ {
+ object xValue = ModUtils.GetSettingUnderlyingValue(x.Value);
+ object yValue = ModUtils.GetSettingUnderlyingValue(y.Value);
+
+ return x.Key == y.Key && EqualityComparer