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/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/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/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.Default.Equals(xValue, yValue); + } + + public int GetHashCode(KeyValuePair obj) => HashCode.Combine(obj.Key, ModUtils.GetSettingUnderlyingValue(obj.Value)); + } } } diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index dd854acc32..81ecc74ddb 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -3,11 +3,10 @@ using System.Buffers; using System.Collections.Generic; -using System.Diagnostics; using System.Text; using MessagePack; using MessagePack.Formatters; -using osu.Framework.Bindables; +using osu.Game.Utils; namespace osu.Game.Online.API { @@ -24,36 +23,7 @@ namespace osu.Game.Online.API var stringBytes = new ReadOnlySequence(Encoding.UTF8.GetBytes(kvp.Key)); writer.WriteString(in stringBytes); - switch (kvp.Value) - { - case Bindable d: - primitiveFormatter.Serialize(ref writer, d.Value, options); - break; - - case Bindable i: - primitiveFormatter.Serialize(ref writer, i.Value, options); - break; - - case Bindable f: - primitiveFormatter.Serialize(ref writer, f.Value, options); - break; - - case Bindable b: - primitiveFormatter.Serialize(ref writer, b.Value, options); - break; - - case IBindable u: - // A mod with unknown (e.g. enum) generic type. - var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); - Debug.Assert(valueMethod != null); - primitiveFormatter.Serialize(ref writer, valueMethod.GetValue(u), options); - break; - - default: - // fall back for non-bindable cases. - primitiveFormatter.Serialize(ref writer, kvp.Value, options); - break; - } + primitiveFormatter.Serialize(ref writer, ModUtils.GetSettingUnderlyingValue(kvp.Value), options); } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 2a11c92223..832a14ee1e 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.IO.Serialization; using osu.Game.Rulesets.UI; +using osu.Game.Utils; namespace osu.Game.Rulesets.Mods { @@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Mods /// The base class for gameplay modifiers. /// [ExcludeFromDynamicCompile] - public abstract class Mod : IMod, IJsonSerializable + public abstract class Mod : IMod, IEquatable, IJsonSerializable { /// /// The name of this mod. @@ -172,7 +173,19 @@ namespace osu.Game.Rulesets.Mods target.Parse(source); } - public bool Equals(IMod other) => GetType() == other?.GetType(); + public bool Equals(IMod other) => other is Mod them && Equals(them); + + public bool Equals(Mod other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return GetType() == other.GetType() && + this.GetSettingsSourceProperties().All(pair => + EqualityComparer.Default.Equals( + ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(this)), + ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(other)))); + } /// /// Reset all custom settings for this mod back to their defaults. diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs index b0ecffdd24..53a1f94731 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osuTK; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations { @@ -12,7 +11,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations /// public class PointVisualisation : Box { - public const float WIDTH = 1; + public const float MAX_WIDTH = 4; public PointVisualisation(double startTime) : this() @@ -27,8 +26,11 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Y; - Width = WIDTH; - EdgeSmoothness = new Vector2(WIDTH, 0); + Anchor = Anchor.CentreLeft; + Origin = Anchor.Centre; + + Width = MAX_WIDTH; + Height = 0.75f; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index c070c833f8..3aaf0451c8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -6,9 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -33,6 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private OsuColour colours { get; set; } + private static readonly int highest_divisor = BindableBeatDivisor.VALID_DIVISORS.Last(); + public TimelineTickDisplay() { RelativeSizeAxes = Axes.Both; @@ -80,8 +80,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (timeline != null) { var newRange = ( - (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X, - (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X); + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X, + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X); if (visibleRange != newRange) { @@ -100,7 +100,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void createTicks() { int drawableIndex = 0; - int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last(); nextMinTick = null; nextMaxTick = null; @@ -131,25 +130,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); var colour = BindableBeatDivisor.GetColourFor(divisor, colours); - bool isMainBeat = indexInBar == 0; - // even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn. - float height = isMainBeat ? 0.5f : 0.4f - (float)divisor / highestDivisor * 0.2f; - float gradientOpacity = isMainBeat ? 1 : 0; - var topPoint = getNextUsablePoint(); - topPoint.X = xPos; - topPoint.Height = height; - topPoint.Colour = ColourInfo.GradientVertical(colour, colour.Opacity(gradientOpacity)); - topPoint.Anchor = Anchor.TopLeft; - topPoint.Origin = Anchor.TopCentre; - - var bottomPoint = getNextUsablePoint(); - bottomPoint.X = xPos; - bottomPoint.Anchor = Anchor.BottomLeft; - bottomPoint.Colour = ColourInfo.GradientVertical(colour.Opacity(gradientOpacity), colour); - bottomPoint.Origin = Anchor.BottomCentre; - bottomPoint.Height = height; + var line = getNextUsableLine(); + line.X = xPos; + line.Width = PointVisualisation.MAX_WIDTH * getWidth(indexInBar, divisor); + line.Height = 0.9f * getHeight(indexInBar, divisor); + line.Colour = colour; } beat++; @@ -168,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline tickCache.Validate(); - Drawable getNextUsablePoint() + Drawable getNextUsableLine() { PointVisualisation point; if (drawableIndex >= Count) @@ -183,6 +170,54 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } + private static float getWidth(int indexInBar, int divisor) + { + if (indexInBar == 0) + return 1; + + switch (divisor) + { + case 1: + case 2: + return 0.6f; + + case 3: + case 4: + return 0.5f; + + case 6: + case 8: + return 0.4f; + + default: + return 0.3f; + } + } + + private static float getHeight(int indexInBar, int divisor) + { + if (indexInBar == 0) + return 1; + + switch (divisor) + { + case 1: + case 2: + return 0.9f; + + case 3: + case 4: + return 0.8f; + + case 6: + case 8: + return 0.7f; + + default: + return 0.6f; + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index d22199447d..b64d835c58 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -109,7 +109,12 @@ namespace osu.Game.Screens.Play request.Success += s => { - score.ScoreInfo.OnlineScoreID = s.ID; + // For the time being, online ID responses are not really useful for anything. + // In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores. + // + // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint + // conflicts across various systems (ie. solo and multiplayer). + // score.ScoreInfo.OnlineScoreID = s.ID; tcs.SetResult(true); }; diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index c12b5a9fd4..596880f2e7 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using osu.Framework.Bindables; +using osu.Game.Online.API; using osu.Game.Rulesets.Mods; #nullable enable @@ -129,5 +132,38 @@ namespace osu.Game.Utils else yield return mod; } + + /// + /// Returns the underlying value of the given mod setting object. + /// Used in for serialization and equality comparison purposes. + /// + /// The mod setting. + public static object GetSettingUnderlyingValue(object setting) + { + switch (setting) + { + case Bindable d: + return d.Value; + + case Bindable i: + return i.Value; + + case Bindable f: + return f.Value; + + case Bindable b: + return b.Value; + + case IBindable u: + // A mod with unknown (e.g. enum) generic type. + var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); + Debug.Assert(valueMethod != null); + return valueMethod.GetValue(u); + + default: + // fall back for non-bindable cases. + return setting; + } + } } }