diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 8b5431e2d6..e779ee6658 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -19,3 +19,7 @@ P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResult
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
+M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead.
+M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
+M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.
+M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead.
diff --git a/osu.Android.props b/osu.Android.props
index 3b14d85e53..c83b7872ac 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index cebbcb40b7..19cf7f5d46 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -37,9 +37,15 @@ namespace osu.Desktop
// See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
{
+ // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
+ // disabling it ourselves.
+ // We could also better detect compatibility mode if required:
+ // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!",
- "This version of osu! requires at least Windows 8.1 to run.\nPlease upgrade your operating system or consider using an older version of osu!.", IntPtr.Zero);
+ "This version of osu! requires at least Windows 8.1 to run.\n"
+ + "Please upgrade your operating system or consider using an older version of osu!.\n\n"
+ + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero);
return;
}
diff --git a/osu.Game.Benchmarks/BenchmarkHitObject.cs b/osu.Game.Benchmarks/BenchmarkHitObject.cs
new file mode 100644
index 0000000000..65c78e39b3
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkHitObject.cs
@@ -0,0 +1,166 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using BenchmarkDotNet.Attributes;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Benchmarks
+{
+ public class BenchmarkHitObject : BenchmarkTest
+ {
+ [Params(1, 100, 1000)]
+ public int Count { get; set; }
+
+ [Params(false, true)]
+ public bool WithBindableAccess { get; set; }
+
+ [Benchmark]
+ public HitCircle[] OsuCircle()
+ {
+ var circles = new HitCircle[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ circles[i] = new HitCircle();
+
+ if (WithBindableAccess)
+ {
+ _ = circles[i].PositionBindable;
+ _ = circles[i].ScaleBindable;
+ _ = circles[i].ComboIndexBindable;
+ _ = circles[i].ComboOffsetBindable;
+ _ = circles[i].StackHeightBindable;
+ _ = circles[i].LastInComboBindable;
+ _ = circles[i].ComboIndexWithOffsetsBindable;
+ _ = circles[i].IndexInCurrentComboBindable;
+ _ = circles[i].SamplesBindable;
+ _ = circles[i].StartTimeBindable;
+ }
+ else
+ {
+ _ = circles[i].Position;
+ _ = circles[i].Scale;
+ _ = circles[i].ComboIndex;
+ _ = circles[i].ComboOffset;
+ _ = circles[i].StackHeight;
+ _ = circles[i].LastInCombo;
+ _ = circles[i].ComboIndexWithOffsets;
+ _ = circles[i].IndexInCurrentCombo;
+ _ = circles[i].Samples;
+ _ = circles[i].StartTime;
+ _ = circles[i].Position;
+ _ = circles[i].Scale;
+ _ = circles[i].ComboIndex;
+ _ = circles[i].ComboOffset;
+ _ = circles[i].StackHeight;
+ _ = circles[i].LastInCombo;
+ _ = circles[i].ComboIndexWithOffsets;
+ _ = circles[i].IndexInCurrentCombo;
+ _ = circles[i].Samples;
+ _ = circles[i].StartTime;
+ }
+ }
+
+ return circles;
+ }
+
+ [Benchmark]
+ public Hit[] TaikoHit()
+ {
+ var hits = new Hit[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ hits[i] = new Hit();
+
+ if (WithBindableAccess)
+ {
+ _ = hits[i].TypeBindable;
+ _ = hits[i].IsStrongBindable;
+ _ = hits[i].SamplesBindable;
+ _ = hits[i].StartTimeBindable;
+ }
+ else
+ {
+ _ = hits[i].Type;
+ _ = hits[i].IsStrong;
+ _ = hits[i].Samples;
+ _ = hits[i].StartTime;
+ }
+ }
+
+ return hits;
+ }
+
+ [Benchmark]
+ public Fruit[] CatchFruit()
+ {
+ var fruit = new Fruit[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ fruit[i] = new Fruit();
+
+ if (WithBindableAccess)
+ {
+ _ = fruit[i].OriginalXBindable;
+ _ = fruit[i].XOffsetBindable;
+ _ = fruit[i].ScaleBindable;
+ _ = fruit[i].ComboIndexBindable;
+ _ = fruit[i].HyperDashBindable;
+ _ = fruit[i].LastInComboBindable;
+ _ = fruit[i].ComboIndexWithOffsetsBindable;
+ _ = fruit[i].IndexInCurrentComboBindable;
+ _ = fruit[i].IndexInBeatmapBindable;
+ _ = fruit[i].SamplesBindable;
+ _ = fruit[i].StartTimeBindable;
+ }
+ else
+ {
+ _ = fruit[i].OriginalX;
+ _ = fruit[i].XOffset;
+ _ = fruit[i].Scale;
+ _ = fruit[i].ComboIndex;
+ _ = fruit[i].HyperDash;
+ _ = fruit[i].LastInCombo;
+ _ = fruit[i].ComboIndexWithOffsets;
+ _ = fruit[i].IndexInCurrentCombo;
+ _ = fruit[i].IndexInBeatmap;
+ _ = fruit[i].Samples;
+ _ = fruit[i].StartTime;
+ }
+ }
+
+ return fruit;
+ }
+
+ [Benchmark]
+ public Note[] ManiaNote()
+ {
+ var notes = new Note[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ notes[i] = new Note();
+
+ if (WithBindableAccess)
+ {
+ _ = notes[i].ColumnBindable;
+ _ = notes[i].SamplesBindable;
+ _ = notes[i].StartTimeBindable;
+ }
+ else
+ {
+ _ = notes[i].Column;
+ _ = notes[i].Samples;
+ _ = notes[i].StartTime;
+ }
+ }
+
+ return notes;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index f5a3426305..6e01c44e1f 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -19,7 +19,9 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public const float OBJECT_RADIUS = 64;
- public readonly Bindable OriginalXBindable = new Bindable();
+ private HitObjectProperty originalX;
+
+ public Bindable OriginalXBindable => originalX.Bindable;
///
/// The horizontal position of the hit object between 0 and .
@@ -31,18 +33,20 @@ namespace osu.Game.Rulesets.Catch.Objects
[JsonIgnore]
public float X
{
- set => OriginalXBindable.Value = value;
+ set => originalX.Value = value;
}
- public readonly Bindable XOffsetBindable = new Bindable();
+ private HitObjectProperty xOffset;
+
+ public Bindable XOffsetBindable => xOffset.Bindable;
///
/// A random offset applied to the horizontal position, set by the beatmap processing.
///
public float XOffset
{
- get => XOffsetBindable.Value;
- set => XOffsetBindable.Value = value;
+ get => xOffset.Value;
+ set => xOffset.Value = value;
}
///
@@ -54,8 +58,8 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public float OriginalX
{
- get => OriginalXBindable.Value;
- set => OriginalXBindable.Value = value;
+ get => originalX.Value;
+ set => originalX.Value = value;
}
///
@@ -69,59 +73,71 @@ namespace osu.Game.Rulesets.Catch.Objects
public double TimePreempt { get; set; } = 1000;
- public readonly Bindable IndexInBeatmapBindable = new Bindable();
+ private HitObjectProperty indexInBeatmap;
+
+ public Bindable IndexInBeatmapBindable => indexInBeatmap.Bindable;
public int IndexInBeatmap
{
- get => IndexInBeatmapBindable.Value;
- set => IndexInBeatmapBindable.Value = value;
+ get => indexInBeatmap.Value;
+ set => indexInBeatmap.Value = value;
}
public virtual bool NewCombo { get; set; }
public int ComboOffset { get; set; }
- public Bindable IndexInCurrentComboBindable { get; } = new Bindable();
+ private HitObjectProperty indexInCurrentCombo;
+
+ public Bindable IndexInCurrentComboBindable => indexInCurrentCombo.Bindable;
public int IndexInCurrentCombo
{
- get => IndexInCurrentComboBindable.Value;
- set => IndexInCurrentComboBindable.Value = value;
+ get => indexInCurrentCombo.Value;
+ set => indexInCurrentCombo.Value = value;
}
- public Bindable ComboIndexBindable { get; } = new Bindable();
+ private HitObjectProperty comboIndex;
+
+ public Bindable ComboIndexBindable => comboIndex.Bindable;
public int ComboIndex
{
- get => ComboIndexBindable.Value;
- set => ComboIndexBindable.Value = value;
+ get => comboIndex.Value;
+ set => comboIndex.Value = value;
}
- public Bindable ComboIndexWithOffsetsBindable { get; } = new Bindable();
+ private HitObjectProperty comboIndexWithOffsets;
+
+ public Bindable ComboIndexWithOffsetsBindable => comboIndexWithOffsets.Bindable;
public int ComboIndexWithOffsets
{
- get => ComboIndexWithOffsetsBindable.Value;
- set => ComboIndexWithOffsetsBindable.Value = value;
+ get => comboIndexWithOffsets.Value;
+ set => comboIndexWithOffsets.Value = value;
}
- public Bindable LastInComboBindable { get; } = new Bindable();
+ private HitObjectProperty lastInCombo;
+
+ public Bindable LastInComboBindable => lastInCombo.Bindable;
///
/// The next fruit starts a new combo. Used for explodey.
///
public virtual bool LastInCombo
{
- get => LastInComboBindable.Value;
- set => LastInComboBindable.Value = value;
+ get => lastInCombo.Value;
+ set => lastInCombo.Value = value;
}
- public readonly Bindable ScaleBindable = new Bindable(1);
+ private HitObjectProperty scale = new HitObjectProperty(1);
+
+ public Bindable ScaleBindable => scale.Bindable;
public float Scale
{
- get => ScaleBindable.Value;
- set => ScaleBindable.Value = value;
+ get => scale.Value;
+ set => scale.Value = value;
}
///
diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
index 1ededa1438..c9bc9ca2ac 100644
--- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
@@ -5,6 +5,7 @@
using Newtonsoft.Json;
using osu.Framework.Bindables;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osuTK.Graphics;
@@ -24,12 +25,14 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public float DistanceToHyperDash { get; set; }
- public readonly Bindable HyperDashBindable = new Bindable();
+ private HitObjectProperty hyperDash;
+
+ public Bindable HyperDashBindable => hyperDash.Bindable;
///
/// Whether this fruit can initiate a hyperdash.
///
- public bool HyperDash => HyperDashBindable.Value;
+ public bool HyperDash => hyperDash.Value;
private CatchHitObject hyperDashTarget;
diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
index 0efaeac026..ebff5cf4e9 100644
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
@@ -13,12 +13,14 @@ namespace osu.Game.Rulesets.Mania.Objects
{
public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition
{
- public readonly Bindable ColumnBindable = new Bindable();
+ private HitObjectProperty column;
+
+ public Bindable ColumnBindable => column.Bindable;
public virtual int Column
{
- get => ColumnBindable.Value;
- set => ColumnBindable.Value = value;
+ get => column.Value;
+ set => column.Value = value;
}
protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index bb593c2fb3..46f7c461f8 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.6972307565739273d, 206, "diffcalc-test")]
- [TestCase(1.4484754139145539d, 45, "zero-length-sliders")]
+ [TestCase(6.6369583000323935d, 206, "diffcalc-test")]
+ [TestCase(1.4476531024675374d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(8.9382559208689809d, 206, "diffcalc-test")]
- [TestCase(1.7548875851757628d, 45, "zero-length-sliders")]
+ [TestCase(8.8816128335486386d, 206, "diffcalc-test")]
+ [TestCase(1.7540389962596916d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.6972307218715166d, 239, "diffcalc-test")]
- [TestCase(1.4484754139145537d, 54, "zero-length-sliders")]
+ [TestCase(6.6369583000323935d, 239, "diffcalc-test")]
+ [TestCase(1.4476531024675374d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index 0694746cbf..76d5ccf682 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -108,13 +108,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
- // Reward for % distance slowed down compared to previous, paying attention to not award overlap
- double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
- // do not award overlap
- * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2);
-
- // Choose the largest bonus, multiplied by ratio.
- velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio;
+ velocityChangeBonus = overlapVelocityBuff * distRatio;
// Penalize for rhythm changes.
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 139bfe7dd3..59be93530c 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
@@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public class SliderPlacementBlueprint : PlacementBlueprint
{
- public new Objects.Slider HitObject => (Objects.Slider)base.HitObject;
+ public new Slider HitObject => (Slider)base.HitObject;
private SliderBodyPiece bodyPiece;
private HitCirclePiece headCirclePiece;
@@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private IDistanceSnapProvider snapProvider { get; set; }
public SliderPlacementBlueprint()
- : base(new Objects.Slider())
+ : base(new Slider())
{
RelativeSizeAxes = Axes.Both;
@@ -82,7 +83,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.Initial:
BeginPlacement();
- var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
+ var nearestDifficultyPoint = editorBeatmap.HitObjects
+ .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime)?
+ .DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 387342b4a9..7b98fc48e0 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -7,12 +7,12 @@ using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Objects;
-using osuTK;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects
{
@@ -36,12 +36,14 @@ namespace osu.Game.Rulesets.Osu.Objects
public double TimePreempt = 600;
public double TimeFadeIn = 400;
- public readonly Bindable PositionBindable = new Bindable();
+ private HitObjectProperty position;
+
+ public Bindable PositionBindable => position.Bindable;
public virtual Vector2 Position
{
- get => PositionBindable.Value;
- set => PositionBindable.Value = value;
+ get => position.Value;
+ set => position.Value = value;
}
public float X => Position.X;
@@ -53,66 +55,80 @@ namespace osu.Game.Rulesets.Osu.Objects
public Vector2 StackedEndPosition => EndPosition + StackOffset;
- public readonly Bindable StackHeightBindable = new Bindable();
+ private HitObjectProperty stackHeight;
+
+ public Bindable StackHeightBindable => stackHeight.Bindable;
public int StackHeight
{
- get => StackHeightBindable.Value;
- set => StackHeightBindable.Value = value;
+ get => stackHeight.Value;
+ set => stackHeight.Value = value;
}
public virtual Vector2 StackOffset => new Vector2(StackHeight * Scale * -6.4f);
public double Radius => OBJECT_RADIUS * Scale;
- public readonly Bindable ScaleBindable = new BindableFloat(1);
+ private HitObjectProperty scale = new HitObjectProperty(1);
+
+ public Bindable ScaleBindable => scale.Bindable;
public float Scale
{
- get => ScaleBindable.Value;
- set => ScaleBindable.Value = value;
+ get => scale.Value;
+ set => scale.Value = value;
}
public virtual bool NewCombo { get; set; }
- public readonly Bindable ComboOffsetBindable = new Bindable();
+ private HitObjectProperty comboOffset;
+
+ public Bindable ComboOffsetBindable => comboOffset.Bindable;
public int ComboOffset
{
- get => ComboOffsetBindable.Value;
- set => ComboOffsetBindable.Value = value;
+ get => comboOffset.Value;
+ set => comboOffset.Value = value;
}
- public Bindable IndexInCurrentComboBindable { get; } = new Bindable();
+ private HitObjectProperty indexInCurrentCombo;
+
+ public Bindable IndexInCurrentComboBindable => indexInCurrentCombo.Bindable;
public virtual int IndexInCurrentCombo
{
- get => IndexInCurrentComboBindable.Value;
- set => IndexInCurrentComboBindable.Value = value;
+ get => indexInCurrentCombo.Value;
+ set => indexInCurrentCombo.Value = value;
}
- public Bindable ComboIndexBindable { get; } = new Bindable();
+ private HitObjectProperty comboIndex;
+
+ public Bindable ComboIndexBindable => comboIndex.Bindable;
public virtual int ComboIndex
{
- get => ComboIndexBindable.Value;
- set => ComboIndexBindable.Value = value;
+ get => comboIndex.Value;
+ set => comboIndex.Value = value;
}
- public Bindable ComboIndexWithOffsetsBindable { get; } = new Bindable();
+ private HitObjectProperty comboIndexWithOffsets;
+
+ public Bindable ComboIndexWithOffsetsBindable => comboIndexWithOffsets.Bindable;
public int ComboIndexWithOffsets
{
- get => ComboIndexWithOffsetsBindable.Value;
- set => ComboIndexWithOffsetsBindable.Value = value;
+ get => comboIndexWithOffsets.Value;
+ set => comboIndexWithOffsets.Value = value;
}
- public Bindable LastInComboBindable { get; } = new Bindable();
+ private HitObjectProperty lastInCombo;
+
+ public Bindable LastInComboBindable => lastInCombo.Bindable;
public bool LastInCombo
{
- get => LastInComboBindable.Value;
- set => LastInComboBindable.Value = value;
+ get => lastInCombo.Value;
+ set => lastInCombo.Value = value;
}
protected OsuHitObject()
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
index 254e220996..b77d4addee 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK.Graphics;
@@ -32,19 +34,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
protected override void OnTrackingChanged(ValueChangedEvent tracking)
{
- const float scale_duration = 300f;
- const float fade_duration = 300f;
+ Debug.Assert(ParentObject != null);
- this.ScaleTo(tracking.NewValue ? DrawableSliderBall.FOLLOW_AREA : 1f, scale_duration, Easing.OutQuint)
- .FadeTo(tracking.NewValue ? 1f : 0, fade_duration, Easing.OutQuint);
+ const float duration = 300f;
+
+ if (ParentObject.Judged)
+ return;
+
+ if (tracking.NewValue)
+ {
+ if (Precision.AlmostEquals(0, Alpha))
+ this.ScaleTo(1);
+
+ this.ScaleTo(DrawableSliderBall.FOLLOW_AREA, duration, Easing.OutQuint)
+ .FadeTo(1f, duration, Easing.OutQuint);
+ }
+ else
+ {
+ this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.2f, duration / 2, Easing.OutQuint)
+ .FadeTo(0, duration / 2, Easing.OutQuint);
+ }
}
protected override void OnSliderEnd()
{
- const float fade_duration = 450f;
+ const float fade_duration = 300;
// intentionally pile on an extra FadeOut to make it happen much faster
- this.FadeOut(fade_duration / 4, Easing.Out);
+ this.ScaleTo(1, fade_duration, Easing.OutQuint);
+ this.FadeOut(fade_duration / 2, Easing.OutQuint);
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index a9cde62f44..2c2dbddf13 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -35,13 +35,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
- double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
-
- if (score.Mods.Any(m => m is ModNoFail))
- multiplier *= 0.90;
+ double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (score.Mods.Any(m => m is ModHidden))
- multiplier *= 1.10;
+ multiplier *= 1.075;
+
+ if (score.Mods.Any(m => m is ModEasy))
+ multiplier *= 0.975;
double difficultyValue = computeDifficultyValue(score, taikoAttributes);
double accuracyValue = computeAccuracyValue(score, taikoAttributes);
@@ -61,12 +61,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{
- double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.175) - 4.0, 2.25) / 450.0;
+ double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus;
- difficultyValue *= Math.Pow(0.985, countMiss);
+ difficultyValue *= Math.Pow(0.986, countMiss);
+
+ if (score.Mods.Any(m => m is ModEasy))
+ difficultyValue *= 0.980;
if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025;
@@ -74,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModFlashlight))
difficultyValue *= 1.05 * lengthBonus;
- return difficultyValue * score.Accuracy;
+ return difficultyValue * Math.Pow(score.Accuracy, 1.5);
}
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
@@ -82,10 +85,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (attributes.GreatHitWindow <= 0)
return 0;
- double accValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 22.0;
+ double accuracyValue = Math.Pow(140.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 12.0) * 27;
- // Bonus for many objects - it's harder to keep good accuracy up for longer
- return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
+ double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
+ accuracyValue *= lengthBonus;
+
+ // Slight HDFL Bonus for accuracy.
+ if (score.Mods.Any(m => m is ModFlashlight) && score.Mods.Any(m => m is ModHidden))
+ accuracyValue *= 1.10 * lengthBonus;
+
+ return accuracyValue;
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
diff --git a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
index 382035119e..d2eba0eb54 100644
--- a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
@@ -11,14 +11,16 @@ namespace osu.Game.Rulesets.Taiko.Objects
{
public class BarLine : TaikoHitObject, IBarLine
{
+ private HitObjectProperty major;
+
+ public Bindable MajorBindable => major.Bindable;
+
public bool Major
{
- get => MajorBindable.Value;
- set => MajorBindable.Value = value;
+ get => major.Value;
+ set => major.Value = value;
}
- public readonly Bindable MajorBindable = new BindableBool();
-
public override Judgement CreateJudgement() => new IgnoreJudgement();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs
index 20f3304c30..787079bfee 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs
@@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Audio;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
@@ -14,19 +15,21 @@ namespace osu.Game.Rulesets.Taiko.Objects
{
public class Hit : TaikoStrongableHitObject, IHasDisplayColour
{
- public readonly Bindable TypeBindable = new Bindable();
+ private HitObjectProperty type;
- public Bindable DisplayColour { get; } = new Bindable(COLOUR_CENTRE);
+ public Bindable TypeBindable => type.Bindable;
///
/// The that actuates this .
///
public HitType Type
{
- get => TypeBindable.Value;
- set => TypeBindable.Value = value;
+ get => type.Value;
+ set => type.Value = value;
}
+ public Bindable DisplayColour { get; } = new Bindable(COLOUR_CENTRE);
+
public static readonly Color4 COLOUR_CENTRE = Color4Extensions.FromHex(@"bb1177");
public static readonly Color4 COLOUR_RIM = Color4Extensions.FromHex(@"2299bb");
diff --git a/osu.Game.Tests/Extensions/StringDehumanizeExtensionsTest.cs b/osu.Game.Tests/Extensions/StringDehumanizeExtensionsTest.cs
new file mode 100644
index 0000000000..e7490b461b
--- /dev/null
+++ b/osu.Game.Tests/Extensions/StringDehumanizeExtensionsTest.cs
@@ -0,0 +1,85 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Extensions;
+
+namespace osu.Game.Tests.Extensions
+{
+ [TestFixture]
+ public class StringDehumanizeExtensionsTest
+ {
+ [Test]
+ [TestCase("single", "Single")]
+ [TestCase("example word", "ExampleWord")]
+ [TestCase("mixed Casing test", "MixedCasingTest")]
+ [TestCase("PascalCase", "PascalCase")]
+ [TestCase("camelCase", "CamelCase")]
+ [TestCase("snake_case", "SnakeCase")]
+ [TestCase("kebab-case", "KebabCase")]
+ [TestCase("i will not break in a different culture", "IWillNotBreakInADifferentCulture", "tr-TR")]
+ public void TestToPascalCase(string input, string expectedOutput, string? culture = null)
+ {
+ using (temporaryCurrentCulture(culture))
+ Assert.That(input.ToPascalCase(), Is.EqualTo(expectedOutput));
+ }
+
+ [Test]
+ [TestCase("single", "single")]
+ [TestCase("example word", "exampleWord")]
+ [TestCase("mixed Casing test", "mixedCasingTest")]
+ [TestCase("PascalCase", "pascalCase")]
+ [TestCase("camelCase", "camelCase")]
+ [TestCase("snake_case", "snakeCase")]
+ [TestCase("kebab-case", "kebabCase")]
+ [TestCase("I will not break in a different culture", "iWillNotBreakInADifferentCulture", "tr-TR")]
+ public void TestToCamelCase(string input, string expectedOutput, string? culture = null)
+ {
+ using (temporaryCurrentCulture(culture))
+ Assert.That(input.ToCamelCase(), Is.EqualTo(expectedOutput));
+ }
+
+ [Test]
+ [TestCase("single", "single")]
+ [TestCase("example word", "example_word")]
+ [TestCase("mixed Casing test", "mixed_casing_test")]
+ [TestCase("PascalCase", "pascal_case")]
+ [TestCase("camelCase", "camel_case")]
+ [TestCase("snake_case", "snake_case")]
+ [TestCase("kebab-case", "kebab_case")]
+ [TestCase("I will not break in a different culture", "i_will_not_break_in_a_different_culture", "tr-TR")]
+ public void TestToSnakeCase(string input, string expectedOutput, string? culture = null)
+ {
+ using (temporaryCurrentCulture(culture))
+ Assert.That(input.ToSnakeCase(), Is.EqualTo(expectedOutput));
+ }
+
+ [Test]
+ [TestCase("single", "single")]
+ [TestCase("example word", "example-word")]
+ [TestCase("mixed Casing test", "mixed-casing-test")]
+ [TestCase("PascalCase", "pascal-case")]
+ [TestCase("camelCase", "camel-case")]
+ [TestCase("snake_case", "snake-case")]
+ [TestCase("kebab-case", "kebab-case")]
+ [TestCase("I will not break in a different culture", "i-will-not-break-in-a-different-culture", "tr-TR")]
+ public void TestToKebabCase(string input, string expectedOutput, string? culture = null)
+ {
+ using (temporaryCurrentCulture(culture))
+ Assert.That(input.ToKebabCase(), Is.EqualTo(expectedOutput));
+ }
+
+ private IDisposable temporaryCurrentCulture(string? cultureName)
+ {
+ var storedCulture = CultureInfo.CurrentCulture;
+
+ if (cultureName != null)
+ CultureInfo.CurrentCulture = new CultureInfo(cultureName);
+
+ return new InvokeOnDisposal(() => CultureInfo.CurrentCulture = storedCulture);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
index a6f68b2836..4101652c49 100644
--- a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
+++ b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
@@ -17,7 +15,7 @@ namespace osu.Game.Tests.Mods
[TestFixture]
public class ModDifficultyAdjustTest
{
- private TestModDifficultyAdjust testMod;
+ private TestModDifficultyAdjust testMod = null!;
[SetUp]
public void Setup()
@@ -148,7 +146,7 @@ namespace osu.Game.Tests.Mods
yield return new TestModDifficultyAdjust();
}
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null)
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null)
{
throw new System.NotImplementedException();
}
diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
index e94ee40acd..cd6879cf01 100644
--- a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
+++ b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs
index 607b585d33..b9ea1f2567 100644
--- a/osu.Game.Tests/Mods/ModSettingsTest.cs
+++ b/osu.Game.Tests/Mods/ModSettingsTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs
index 22be1a3f01..3b391f6756 100644
--- a/osu.Game.Tests/Mods/ModUtilsTest.cs
+++ b/osu.Game.Tests/Mods/ModUtilsTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using Moq;
@@ -164,19 +162,19 @@ namespace osu.Game.Tests.Mods
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() },
- null
+ Array.Empty()
},
// invalid free mod is valid for local.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
- null
+ Array.Empty()
},
// valid pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
- null
+ Array.Empty()
},
};
@@ -216,13 +214,13 @@ namespace osu.Game.Tests.Mods
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
- null
+ Array.Empty()
},
// valid pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
- null
+ Array.Empty()
},
};
@@ -256,19 +254,19 @@ namespace osu.Game.Tests.Mods
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
- null,
+ Array.Empty(),
},
// incompatible pair with derived class is valid for free mods.
new object[]
{
new Mod[] { new OsuModDeflate(), new OsuModSpinIn() },
- null,
+ Array.Empty(),
},
// valid pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
- null
+ Array.Empty()
},
};
@@ -277,12 +275,12 @@ namespace osu.Game.Tests.Mods
{
bool isValid = ModUtils.CheckValidForGameplay(inputMods, out var invalid);
- Assert.That(isValid, Is.EqualTo(expectedInvalid == null));
+ Assert.That(isValid, Is.EqualTo(expectedInvalid.Length == 0));
if (isValid)
Assert.IsNull(invalid);
else
- Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
+ Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
[TestCaseSource(nameof(invalid_multiplayer_mod_test_scenarios))]
@@ -290,12 +288,12 @@ namespace osu.Game.Tests.Mods
{
bool isValid = ModUtils.CheckValidRequiredModsForMultiplayer(inputMods, out var invalid);
- Assert.That(isValid, Is.EqualTo(expectedInvalid == null));
+ Assert.That(isValid, Is.EqualTo(expectedInvalid.Length == 0));
if (isValid)
Assert.IsNull(invalid);
else
- Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
+ Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
[TestCaseSource(nameof(invalid_free_mod_test_scenarios))]
@@ -303,12 +301,12 @@ namespace osu.Game.Tests.Mods
{
bool isValid = ModUtils.CheckValidFreeModsForMultiplayer(inputMods, out var invalid);
- Assert.That(isValid, Is.EqualTo(expectedInvalid == null));
+ Assert.That(isValid, Is.EqualTo(expectedInvalid.Length == 0));
if (isValid)
Assert.IsNull(invalid);
else
- Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
+ Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
diff --git a/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
index 3c69adcb59..b8a3828a64 100644
--- a/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
+++ b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -29,10 +27,10 @@ namespace osu.Game.Tests.Mods
[TestCase(typeof(ManiaRuleset))]
public void TestAllMultiModsFromRulesetAreIncompatible(Type rulesetType)
{
- var ruleset = (Ruleset)Activator.CreateInstance(rulesetType);
+ var ruleset = Activator.CreateInstance(rulesetType) as Ruleset;
Assert.That(ruleset, Is.Not.Null);
- var allMultiMods = getMultiMods(ruleset);
+ var allMultiMods = getMultiMods(ruleset!);
Assert.Multiple(() =>
{
diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
index f608d020d4..dd105787fa 100644
--- a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
+++ b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
diff --git a/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs b/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
index 08007503c6..9e3354935a 100644
--- a/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
+++ b/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using osu.Framework.Bindables;
@@ -33,7 +31,7 @@ namespace osu.Game.Tests.Mods
return Array.Empty();
}
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException();
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs b/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs
index d0176da0e9..e7a6e9a543 100644
--- a/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs
+++ b/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs
@@ -80,7 +80,7 @@ namespace osu.Game.Tests.Online
{
AddStep("download beatmap", () => beatmaps.Download(test_db_model));
- AddStep("cancel download from request", () => beatmaps.GetExistingDownload(test_db_model).Cancel());
+ AddStep("cancel download from request", () => beatmaps.GetExistingDownload(test_db_model)!.Cancel());
AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null);
AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled);
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index fcf69bf6f2..536322805b 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -126,10 +126,10 @@ namespace osu.Game.Tests.Online
AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet));
addAvailabilityCheckStep("state downloading 0%", () => BeatmapAvailability.Downloading(0.0f));
- AddStep("set progress 40%", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet)).SetProgress(0.4f));
+ AddStep("set progress 40%", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.SetProgress(0.4f));
addAvailabilityCheckStep("state downloading 40%", () => BeatmapAvailability.Downloading(0.4f));
- AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet)).TriggerSuccess(testBeatmapFile));
+ AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile));
addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
@@ -246,7 +246,7 @@ namespace osu.Game.Tests.Online
=> new TestDownloadRequest(set);
}
- private class TestDownloadRequest : ArchiveDownloadRequest
+ internal class TestDownloadRequest : ArchiveDownloadRequest
{
public new void SetProgress(float progress) => base.SetProgress(progress);
public new void TriggerSuccess(string filename) => base.TriggerSuccess(filename);
diff --git a/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs b/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs
index 2622db464f..4601737558 100644
--- a/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs
+++ b/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Framework.Timing;
@@ -19,8 +17,8 @@ namespace osu.Game.Tests.Rulesets.Mods
private const double start_time = 1000;
private const double duration = 9000;
- private TrackVirtual track;
- private OsuPlayfield playfield;
+ private TrackVirtual track = null!;
+ private OsuPlayfield playfield = null!;
[SetUp]
public void SetUp()
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index a2793acba7..d35887c443 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -402,16 +402,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPlayStartsWithCorrectBeatmapWhileAtSongSelect()
{
- createRoom(() => new Room
+ PlaylistItem? item = null;
+ createRoom(() =>
{
- Name = { Value = "Test Room" },
- Playlist =
+ item = new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
- new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
- {
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID
- }
- }
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID
+ };
+ return new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist = { item }
+ };
});
pressReadyButton();
@@ -419,7 +421,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
- ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.ClientRoom?.Settings.PlaylistItemId);
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
@@ -440,16 +442,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPlayStartsWithCorrectRulesetWhileAtSongSelect()
{
- createRoom(() => new Room
+ PlaylistItem? item = null;
+ createRoom(() =>
{
- Name = { Value = "Test Room" },
- Playlist =
+ item = new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
- new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
- {
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID
- }
- }
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID
+ };
+ return new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist = { item }
+ };
});
pressReadyButton();
@@ -457,7 +461,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
- ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.ClientRoom?.Settings.PlaylistItemId);
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
@@ -478,16 +482,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPlayStartsWithCorrectModsWhileAtSongSelect()
{
- createRoom(() => new Room
+ PlaylistItem? item = null;
+ createRoom(() =>
{
- Name = { Value = "Test Room" },
- Playlist =
+ item = new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
- new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
- {
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID
- }
- }
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID
+ };
+ return new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist = { item }
+ };
});
pressReadyButton();
@@ -495,7 +501,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
- ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.ClientRoom?.Settings.PlaylistItemId);
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
index beca3a8700..864b2b6878 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
@@ -255,18 +255,25 @@ namespace osu.Game.Tests.Visual.Online
};
const int initial_great_count = 2000;
+ const int initial_tick_count = 100;
int greatCount = initial_great_count;
+ int tickCount = initial_tick_count;
foreach (var s in scores.Scores)
{
s.Statistics = new Dictionary
{
- { HitResult.Great, greatCount -= 100 },
+ { HitResult.Great, greatCount },
+ { HitResult.LargeTickHit, tickCount },
{ HitResult.Ok, RNG.Next(100) },
{ HitResult.Meh, RNG.Next(100) },
- { HitResult.Miss, initial_great_count - greatCount }
+ { HitResult.Miss, initial_great_count - greatCount },
+ { HitResult.LargeTickMiss, initial_tick_count - tickCount },
};
+
+ greatCount -= 100;
+ tickCount -= RNG.Next(1, 5);
}
return scores;
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 66b9fa990a..e574ee30fb 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -825,7 +825,8 @@ namespace osu.Game.Tests.Visual.SongSelect
checkVisibleItemCount(true, 15);
}
- private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null, int? count = null, bool randomDifficulties = false)
+ private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null, int? count = null,
+ bool randomDifficulties = false)
{
bool changed = false;
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
new file mode 100644
index 0000000000..a95f145897
--- /dev/null
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
@@ -0,0 +1,154 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Screens.Select;
+using osu.Game.Screens.Select.Carousel;
+using osu.Game.Tests.Online;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.SongSelect
+{
+ [TestFixture]
+ public class TestSceneUpdateBeatmapSetButton : OsuManualInputManagerTestScene
+ {
+ private BeatmapCarousel carousel = null!;
+
+ private TestSceneOnlinePlayBeatmapAvailabilityTracker.TestBeatmapModelDownloader beatmapDownloader = null!;
+
+ private BeatmapSetInfo testBeatmapSetInfo = null!;
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
+ var importer = parent.Get();
+
+ dependencies.CacheAs(beatmapDownloader = new TestSceneOnlinePlayBeatmapAvailabilityTracker.TestBeatmapModelDownloader(importer, API));
+ return dependencies;
+ }
+
+ private UpdateBeatmapSetButton? getUpdateButton() => carousel.ChildrenOfType().SingleOrDefault();
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create carousel", () =>
+ {
+ Child = carousel = new BeatmapCarousel
+ {
+ RelativeSizeAxes = Axes.Both,
+ BeatmapSets = new List
+ {
+ (testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()),
+ }
+ };
+ });
+
+ AddUntilStep("wait for load", () => carousel.BeatmapSetsLoaded);
+
+ AddAssert("update button not visible", () => getUpdateButton() == null);
+ }
+
+ [Test]
+ public void TestDownloadToCompletion()
+ {
+ ArchiveDownloadRequest? downloadRequest = null;
+
+ AddStep("update online hash", () =>
+ {
+ testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
+ testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
+
+ carousel.UpdateBeatmapSet(testBeatmapSetInfo);
+ });
+
+ AddUntilStep("update button visible", () => getUpdateButton() != null);
+
+ AddStep("click button", () => getUpdateButton()?.TriggerClick());
+
+ AddUntilStep("wait for download started", () =>
+ {
+ downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo);
+ return downloadRequest != null;
+ });
+
+ AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false);
+
+ AddUntilStep("progress download to completion", () =>
+ {
+ if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
+ {
+ testRequest.SetProgress(testRequest.Progress + 0.1f);
+
+ if (testRequest.Progress >= 1)
+ {
+ testRequest.TriggerSuccess();
+
+ // usually this would be done by the import process.
+ testBeatmapSetInfo.Beatmaps.First().MD5Hash = "different hash";
+ testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
+
+ // usually this would be done by a realm subscription.
+ carousel.UpdateBeatmapSet(testBeatmapSetInfo);
+ return true;
+ }
+ }
+
+ return false;
+ });
+ }
+
+ [Test]
+ public void TestDownloadFailed()
+ {
+ ArchiveDownloadRequest? downloadRequest = null;
+
+ AddStep("update online hash", () =>
+ {
+ testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
+ testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
+
+ carousel.UpdateBeatmapSet(testBeatmapSetInfo);
+ });
+
+ AddUntilStep("update button visible", () => getUpdateButton() != null);
+
+ AddStep("click button", () => getUpdateButton()?.TriggerClick());
+
+ AddUntilStep("wait for download started", () =>
+ {
+ downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo);
+ return downloadRequest != null;
+ });
+
+ AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false);
+
+ AddUntilStep("progress download to failure", () =>
+ {
+ if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
+ {
+ testRequest.SetProgress(testRequest.Progress + 0.1f);
+
+ if (testRequest.Progress >= 0.5f)
+ {
+ testRequest.TriggerFailure(new Exception());
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ AddUntilStep("wait for button enabled", () => getUpdateButton()?.Enabled.Value == true);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
index 44f2da2b95..e8454e8d0f 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
@@ -4,13 +4,13 @@
#nullable disable
using System.Linq;
-using Humanizer;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Extensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.UserInterface
};
control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true);
- control.General.BindCollectionChanged((_, _) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().Underscore())) : "")}", true);
+ control.General.BindCollectionChanged((_, _) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().ToSnakeCase())) : "")}", true);
control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true);
control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true);
diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs
index 6aaf3d5cc2..efa5562cb8 100644
--- a/osu.Game/Audio/HitSampleInfo.cs
+++ b/osu.Game/Audio/HitSampleInfo.cs
@@ -14,15 +14,15 @@ namespace osu.Game.Audio
[Serializable]
public class HitSampleInfo : ISampleInfo, IEquatable
{
+ public const string HIT_NORMAL = @"hitnormal";
public const string HIT_WHISTLE = @"hitwhistle";
public const string HIT_FINISH = @"hitfinish";
- public const string HIT_NORMAL = @"hitnormal";
public const string HIT_CLAP = @"hitclap";
///
/// All valid sample addition constants.
///
- public static IEnumerable AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH };
+ public static IEnumerable AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP };
///
/// The name of the sample to load.
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 41e89d864e..3ee306cc9a 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -92,6 +92,16 @@ namespace osu.Game.Beatmaps
[Indexed]
public string MD5Hash { get; set; } = string.Empty;
+ public string OnlineMD5Hash { get; set; } = string.Empty;
+
+ public DateTimeOffset? LastOnlineUpdate { get; set; }
+
+ ///
+ /// Whether this beatmap matches the online version, based on fetched online metadata.
+ /// Will return true if no online metadata is available.
+ ///
+ public bool MatchesOnlineVersion => LastOnlineUpdate == null || MD5Hash == OnlineMD5Hash;
+
[JsonIgnore]
public bool Hidden { get; set; }
diff --git a/osu.Game/Beatmaps/BeatmapModelDownloader.cs b/osu.Game/Beatmaps/BeatmapModelDownloader.cs
index 74d583fe7e..4295def5c3 100644
--- a/osu.Game/Beatmaps/BeatmapModelDownloader.cs
+++ b/osu.Game/Beatmaps/BeatmapModelDownloader.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
@@ -14,7 +12,7 @@ namespace osu.Game.Beatmaps
protected override ArchiveDownloadRequest CreateDownloadRequest(IBeatmapSetInfo set, bool minimiseDownloadSize) =>
new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
- public override ArchiveDownloadRequest GetExistingDownload(IBeatmapSetInfo model)
+ public override ArchiveDownloadRequest? GetExistingDownload(IBeatmapSetInfo model)
=> CurrentDownloads.Find(r => r.Model.OnlineID == model.OnlineID);
public BeatmapModelDownloader(IModelImporter beatmapImporter, IAPIProvider api)
diff --git a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
new file mode 100644
index 0000000000..b6968f4e06
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
@@ -0,0 +1,50 @@
+// 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 osu.Framework.Graphics;
+using osu.Game.Database;
+using osu.Game.Online.Metadata;
+
+namespace osu.Game.Beatmaps
+{
+ ///
+ /// Ingests any changes that happen externally to the client, reprocessing as required.
+ ///
+ public class BeatmapOnlineChangeIngest : Component
+ {
+ private readonly BeatmapUpdater beatmapUpdater;
+ private readonly RealmAccess realm;
+ private readonly MetadataClient metadataClient;
+
+ public BeatmapOnlineChangeIngest(BeatmapUpdater beatmapUpdater, RealmAccess realm, MetadataClient metadataClient)
+ {
+ this.beatmapUpdater = beatmapUpdater;
+ this.realm = realm;
+ this.metadataClient = metadataClient;
+
+ metadataClient.ChangedBeatmapSetsArrived += changesDetected;
+ }
+
+ private void changesDetected(int[] beatmapSetIds)
+ {
+ // May want to batch incoming updates further if the background realm operations ever becomes a concern.
+ realm.Run(r =>
+ {
+ foreach (int id in beatmapSetIds)
+ {
+ var matchingSet = r.All().FirstOrDefault(s => s.OnlineID == id);
+
+ if (matchingSet != null)
+ beatmapUpdater.Queue(matchingSet.ToLive(realm));
+ }
+ });
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ metadataClient.ChangedBeatmapSetsArrived -= changesDetected;
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs
index a2eb76cafa..580dcee18c 100644
--- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs
+++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs
@@ -102,6 +102,10 @@ namespace osu.Game.Beatmaps
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
+
+ beatmapInfo.OnlineMD5Hash = res.MD5Hash;
+ beatmapInfo.LastOnlineUpdate = res.LastUpdated;
+
beatmapInfo.OnlineID = res.OnlineID;
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
@@ -190,7 +194,7 @@ namespace osu.Game.Beatmaps
using (var cmd = db.CreateCommand())
{
- cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
+ cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash));
cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID));
@@ -209,9 +213,11 @@ namespace osu.Game.Beatmaps
beatmapInfo.BeatmapSet.Status = status;
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
beatmapInfo.OnlineID = reader.GetInt32(1);
-
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
+ beatmapInfo.OnlineMD5Hash = reader.GetString(4);
+ beatmapInfo.LastOnlineUpdate = reader.GetDateTimeOffset(5);
+
logForModel(set, $"Cached local retrieval for {beatmapInfo}.");
return true;
}
diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs
index 96d95b1a12..ead280a75e 100644
--- a/osu.Game/Beatmaps/BeatmapSetInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs
@@ -93,5 +93,7 @@ namespace osu.Game.Beatmaps
IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps;
IEnumerable IHasNamedFiles.Files => Files;
+
+ public bool AllBeatmapsUpToDate => Beatmaps.All(b => b.MatchesOnlineVersion);
}
}
diff --git a/osu.Game/Beatmaps/BeatmapStatisticIcon.cs b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs
index 58d13a3172..8002910b52 100644
--- a/osu.Game/Beatmaps/BeatmapStatisticIcon.cs
+++ b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs
@@ -3,10 +3,10 @@
#nullable disable
-using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Game.Extensions;
namespace osu.Game.Beatmaps
{
@@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
- Texture = textures.Get($"Icons/BeatmapDetails/{iconType.ToString().Kebaberize()}");
+ Texture = textures.Get($"Icons/BeatmapDetails/{iconType.ToString().ToKebabCase()}");
}
}
diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs
index 20fa0bc7c6..d2c5e5616a 100644
--- a/osu.Game/Beatmaps/BeatmapUpdater.cs
+++ b/osu.Game/Beatmaps/BeatmapUpdater.cs
@@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Online.API;
@@ -30,21 +31,12 @@ namespace osu.Game.Beatmaps
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
}
- ///
- /// Queue a beatmap for background processing.
- ///
- public void Queue(int beatmapSetId)
- {
- // TODO: implement
- }
-
///
/// Queue a beatmap for background processing.
///
public void Queue(Live beatmap)
{
- // For now, just fire off a task.
- // TODO: Add actual queueing probably.
+ Logger.Log($"Queueing change for local beatmap {beatmap}");
Task.Factory.StartNew(() => beatmap.PerformRead(Process));
}
@@ -56,6 +48,8 @@ namespace osu.Game.Beatmaps
// Before we use below, we want to invalidate.
workingBeatmapCache.Invalidate(beatmapSet);
+ // TODO: this call currently uses the local `online.db` lookup.
+ // We probably don't want this to happen after initial import (as the data may be stale).
onlineLookupQueue.Update(beatmapSet);
foreach (var beatmap in beatmapSet.Beatmaps)
diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs
index 76717fd46f..02bcb342e4 100644
--- a/osu.Game/Database/ModelDownloader.cs
+++ b/osu.Game/Database/ModelDownloader.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -19,18 +17,18 @@ namespace osu.Game.Database
where TModel : class, IHasGuidPrimaryKey, ISoftDelete, IEquatable, T
where T : class
{
- public Action PostNotification { protected get; set; }
+ public Action? PostNotification { protected get; set; }
- public event Action> DownloadBegan;
+ public event Action>? DownloadBegan;
- public event Action> DownloadFailed;
+ public event Action>? DownloadFailed;
private readonly IModelImporter importer;
- private readonly IAPIProvider api;
+ private readonly IAPIProvider? api;
protected readonly List> CurrentDownloads = new List>();
- protected ModelDownloader(IModelImporter importer, IAPIProvider api)
+ protected ModelDownloader(IModelImporter importer, IAPIProvider? api)
{
this.importer = importer;
this.api = api;
@@ -87,7 +85,7 @@ namespace osu.Game.Database
CurrentDownloads.Add(request);
PostNotification?.Invoke(notification);
- api.PerformAsync(request);
+ api?.PerformAsync(request);
DownloadBegan?.Invoke(request);
return true;
@@ -105,7 +103,7 @@ namespace osu.Game.Database
}
}
- public abstract ArchiveDownloadRequest GetExistingDownload(T model);
+ public abstract ArchiveDownloadRequest? GetExistingDownload(T model);
private bool canDownload(T model) => GetExistingDownload(model) == null && api != null;
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index c4d65f4f10..28870617cc 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -61,8 +61,9 @@ namespace osu.Game.Database
/// 15 2022-07-13 Added LastPlayed to BeatmapInfo.
/// 16 2022-07-15 Removed HasReplay from ScoreInfo.
/// 17 2022-07-16 Added CountryCode to RealmUser.
+ /// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo.
///
- private const int schema_version = 17;
+ private const int schema_version = 18;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs
index d1aba2bfe3..35f2d61437 100644
--- a/osu.Game/Extensions/DrawableExtensions.cs
+++ b/osu.Game/Extensions/DrawableExtensions.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -67,7 +66,7 @@ namespace osu.Game.Extensions
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
- if (!info.Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
+ if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object settingValue))
continue;
skinnable.CopyAdjustedSetting((IBindable)property.GetValue(component), settingValue);
diff --git a/osu.Game/Extensions/StringDehumanizeExtensions.cs b/osu.Game/Extensions/StringDehumanizeExtensions.cs
new file mode 100644
index 0000000000..6f0d7622d3
--- /dev/null
+++ b/osu.Game/Extensions/StringDehumanizeExtensions.cs
@@ -0,0 +1,94 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+// Based on code from the Humanizer library (https://github.com/Humanizr/Humanizer/blob/606e958cb83afc9be5b36716ac40d4daa9fa73a7/src/Humanizer/InflectorExtensions.cs)
+//
+// Humanizer is licenced under the MIT License (MIT)
+//
+// Copyright (c) .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System.Text.RegularExpressions;
+
+namespace osu.Game.Extensions
+{
+ ///
+ /// Class with extension methods used to turn human-readable strings to casing conventions frequently used in code.
+ /// Often used for communicating with other systems (web API, spectator server).
+ /// All of the operations in this class are intentionally culture-invariant.
+ ///
+ public static class StringDehumanizeExtensions
+ {
+ ///
+ /// Converts the string to "Pascal case" (also known as "upper camel case").
+ ///
+ ///
+ ///
+ /// "this is a test string".ToPascalCase() == "ThisIsATestString"
+ ///
+ ///
+ public static string ToPascalCase(this string input)
+ {
+ return Regex.Replace(input, "(?:^|_|-| +)(.)", match => match.Groups[1].Value.ToUpperInvariant());
+ }
+
+ ///
+ /// Converts the string to (lower) "camel case".
+ ///
+ ///
+ ///
+ /// "this is a test string".ToCamelCase() == "thisIsATestString"
+ ///
+ ///
+ public static string ToCamelCase(this string input)
+ {
+ string word = input.ToPascalCase();
+ return word.Length > 0 ? word.Substring(0, 1).ToLowerInvariant() + word.Substring(1) : word;
+ }
+
+ ///
+ /// Converts the string to "snake case".
+ ///
+ ///
+ ///
+ /// "this is a test string".ToSnakeCase() == "this_is_a_test_string"
+ ///
+ ///
+ public static string ToSnakeCase(this string input)
+ {
+ return Regex.Replace(
+ Regex.Replace(
+ Regex.Replace(input, @"([\p{Lu}]+)([\p{Lu}][\p{Ll}])", "$1_$2"), @"([\p{Ll}\d])([\p{Lu}])", "$1_$2"), @"[-\s]", "_").ToLowerInvariant();
+ }
+
+ ///
+ /// Converts the string to "kebab case".
+ ///
+ ///
+ ///
+ /// "this is a test string".ToKebabCase() == "this-is-a-test-string"
+ ///
+ ///
+ public static string ToKebabCase(this string input)
+ {
+ return ToSnakeCase(input).Replace('_', '-');
+ }
+ }
+}
diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs
index 10ed76ebdd..862a10208c 100644
--- a/osu.Game/Graphics/Cursor/MenuCursor.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursor.cs
@@ -3,21 +3,21 @@
#nullable disable
-using osuTK;
+using System;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
-using osu.Game.Configuration;
-using System;
-using JetBrains.Annotations;
-using osu.Framework.Audio;
-using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
+using osu.Game.Configuration;
+using osuTK;
namespace osu.Game.Graphics.Cursor
{
@@ -35,6 +35,7 @@ namespace osu.Game.Graphics.Cursor
private Vector2 positionMouseDown;
private Sample tapSample;
+ private Vector2 lastMovePosition;
[BackgroundDependencyLoader(true)]
private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager, AudioManager audio)
@@ -47,16 +48,25 @@ namespace osu.Game.Graphics.Cursor
tapSample = audio.Samples.Get(@"UI/cursor-tap");
}
+ protected override void Update()
+ {
+ base.Update();
+
+ if (dragRotationState != DragRotationState.NotDragging
+ && Vector2.Distance(positionMouseDown, lastMovePosition) > 60)
+ {
+ // make the rotation centre point floating.
+ positionMouseDown = Interpolation.ValueAt(0.04f, positionMouseDown, lastMovePosition, 0, Clock.ElapsedFrameTime);
+ }
+ }
+
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (dragRotationState != DragRotationState.NotDragging)
{
- // make the rotation centre point floating.
- if (Vector2.Distance(positionMouseDown, e.MousePosition) > 60)
- positionMouseDown = Interpolation.ValueAt(0.005f, positionMouseDown, e.MousePosition, 0, Clock.ElapsedFrameTime);
+ lastMovePosition = e.MousePosition;
- var position = e.MousePosition;
- float distance = Vector2Extensions.Distance(position, positionMouseDown);
+ float distance = Vector2Extensions.Distance(lastMovePosition, positionMouseDown);
// don't start rotating until we're moved a minimum distance away from the mouse down location,
// else it can have an annoying effect.
diff --git a/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs b/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs
index 4808ac1384..b51a8473ca 100644
--- a/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs
+++ b/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs
@@ -3,8 +3,8 @@
#nullable disable
-using Humanizer;
using Newtonsoft.Json.Serialization;
+using osu.Game.Extensions;
namespace osu.Game.IO.Serialization
{
@@ -12,7 +12,7 @@ namespace osu.Game.IO.Serialization
{
protected override string ResolvePropertyName(string propertyName)
{
- return propertyName.Underscore();
+ return propertyName.ToSnakeCase();
}
}
}
diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs
index 3bd0c91f8e..900f59290c 100644
--- a/osu.Game/Online/API/APIMod.cs
+++ b/osu.Game/Online/API/APIMod.cs
@@ -7,12 +7,12 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using Humanizer;
using MessagePack;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Configuration;
+using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@@ -45,7 +45,7 @@ namespace osu.Game.Online.API
var bindable = (IBindable)property.GetValue(mod);
if (!bindable.IsDefault)
- Settings.Add(property.Name.Underscore(), bindable.GetUnderlyingSettingValue());
+ Settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue());
}
}
@@ -63,7 +63,7 @@ namespace osu.Game.Online.API
{
foreach (var (_, property) in resultMod.GetSettingsSourceProperties())
{
- if (!Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
+ if (!Settings.TryGetValue(property.Name.ToSnakeCase(), out object settingValue))
continue;
try
diff --git a/osu.Game/Online/API/Requests/GetCommentsRequest.cs b/osu.Game/Online/API/Requests/GetCommentsRequest.cs
index c63c574124..1aa08f2ed8 100644
--- a/osu.Game/Online/API/Requests/GetCommentsRequest.cs
+++ b/osu.Game/Online/API/Requests/GetCommentsRequest.cs
@@ -4,7 +4,7 @@
#nullable disable
using osu.Framework.IO.Network;
-using Humanizer;
+using osu.Game.Extensions;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Comments;
@@ -32,7 +32,7 @@ namespace osu.Game.Online.API.Requests
var req = base.CreateWebRequest();
req.AddParameter("commentable_id", commentableId.ToString());
- req.AddParameter("commentable_type", type.ToString().Underscore().ToLowerInvariant());
+ req.AddParameter("commentable_type", type.ToString().ToSnakeCase().ToLowerInvariant());
req.AddParameter("page", page.ToString());
req.AddParameter("sort", sort.ToString().ToLowerInvariant());
diff --git a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs
index 3ec60cd06c..d723786f23 100644
--- a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs
@@ -3,8 +3,8 @@
#nullable disable
-using Humanizer;
using System.Collections.Generic;
+using osu.Game.Extensions;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
@@ -22,7 +22,7 @@ namespace osu.Game.Online.API.Requests
this.type = type;
}
- protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().Underscore()}";
+ protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().ToSnakeCase()}";
}
public enum BeatmapSetType
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
index 735fde333d..3fee81cf33 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
@@ -81,6 +81,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"max_combo")]
public int? MaxCombo { get; set; }
+ [JsonProperty(@"last_updated")]
+ public DateTimeOffset LastUpdated { get; set; }
+
public double BPM { get; set; }
#region Implementation of IBeatmapInfo
diff --git a/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs b/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
index 8fefe4d9c2..2def18926f 100644
--- a/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
@@ -4,8 +4,8 @@
#nullable disable
using System;
-using Humanizer;
using Newtonsoft.Json;
+using osu.Game.Extensions;
using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses
@@ -21,7 +21,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty]
private string type
{
- set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.Pascalize());
+ set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.ToPascalCase());
}
public RecentActivityType Type;
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
index 38c67d92f4..4ef39be5e5 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"scores")]
public List Scores;
- [JsonProperty(@"userScore")]
+ [JsonProperty(@"user_score")]
public APIScoreWithPosition UserScore;
}
}
diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
index 082f9bb371..c303c410ec 100644
--- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
@@ -5,7 +5,6 @@
using System.Collections.Generic;
using System.Linq;
-using Humanizer;
using JetBrains.Annotations;
using osu.Framework.IO.Network;
using osu.Game.Extensions;
@@ -86,7 +85,7 @@ namespace osu.Game.Online.API.Requests
req.AddParameter("q", query);
if (General != null && General.Any())
- req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().Underscore())));
+ req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().ToSnakeCase())));
if (ruleset.OnlineID >= 0)
req.AddParameter("m", ruleset.OnlineID.ToString());
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index 01f0f3a902..6bfe09e911 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -64,26 +64,26 @@ namespace osu.Game.Online
this.preferMessagePack = preferMessagePack;
apiState.BindTo(api.State);
- apiState.BindValueChanged(_ => connectIfPossible(), true);
+ apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
}
- public void Reconnect()
+ public Task Reconnect()
{
Logger.Log($"{clientName} reconnecting...", LoggingTarget.Network);
- Task.Run(connectIfPossible);
+ return Task.Run(connectIfPossible);
}
- private void connectIfPossible()
+ private async Task connectIfPossible()
{
switch (apiState.Value)
{
case APIState.Failing:
case APIState.Offline:
- Task.Run(() => disconnect(true));
+ await disconnect(true);
break;
case APIState.Online:
- Task.Run(connect);
+ await connect();
break;
}
}
diff --git a/osu.Game/Online/IHubClientConnector.cs b/osu.Game/Online/IHubClientConnector.cs
index 2afab9091b..53c4897e73 100644
--- a/osu.Game/Online/IHubClientConnector.cs
+++ b/osu.Game/Online/IHubClientConnector.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Bindables;
using osu.Game.Online.API;
@@ -32,6 +33,6 @@ namespace osu.Game.Online
///
/// Reconnect if already connected.
///
- void Reconnect();
+ Task Reconnect();
}
}
diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs
index 1e5eeb4eb0..60867da2d7 100644
--- a/osu.Game/Online/Metadata/MetadataClient.cs
+++ b/osu.Game/Online/Metadata/MetadataClient.cs
@@ -1,6 +1,8 @@
// 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.Linq;
using System.Threading.Tasks;
using osu.Framework.Graphics;
@@ -11,5 +13,13 @@ namespace osu.Game.Online.Metadata
public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates);
public abstract Task GetChangesSince(int queueId);
+
+ public Action? ChangedBeatmapSetsArrived;
+
+ protected Task ProcessChanges(int[] beatmapSetIDs)
+ {
+ ChangedBeatmapSetsArrived?.Invoke(beatmapSetIDs.Distinct().ToArray());
+ return Task.CompletedTask;
+ }
}
}
diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
index 1b0d1884dc..95228c380f 100644
--- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs
+++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
@@ -7,7 +7,6 @@ using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
-using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.API;
@@ -15,7 +14,6 @@ namespace osu.Game.Online.Metadata
{
public class OnlineMetadataClient : MetadataClient
{
- private readonly BeatmapUpdater beatmapUpdater;
private readonly string endpoint;
private IHubClientConnector? connector;
@@ -24,9 +22,8 @@ namespace osu.Game.Online.Metadata
private HubConnection? connection => connector?.CurrentConnection;
- public OnlineMetadataClient(EndpointConfiguration endpoints, BeatmapUpdater beatmapUpdater)
+ public OnlineMetadataClient(EndpointConfiguration endpoints)
{
- this.beatmapUpdater = beatmapUpdater;
endpoint = endpoints.MetadataEndpointUrl;
}
@@ -102,17 +99,6 @@ namespace osu.Game.Online.Metadata
await ProcessChanges(updates.BeatmapSetIDs);
}
- protected Task ProcessChanges(int[] beatmapSetIDs)
- {
- foreach (int id in beatmapSetIDs)
- {
- Logger.Log($"Processing {id}...");
- beatmapUpdater.Queue(id);
- }
-
- return Task.CompletedTask;
- }
-
public override Task GetChangesSince(int queueId)
{
if (connector?.IsConnected.Value != true)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 9832acb140..603bd10c38 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -21,7 +21,6 @@ using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
-using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Online.Multiplayer
{
@@ -91,7 +90,7 @@ namespace osu.Game.Online.Multiplayer
///
/// The joined .
///
- public virtual MultiplayerRoom? Room
+ public virtual MultiplayerRoom? Room // virtual for moq
{
get
{
@@ -150,7 +149,7 @@ namespace osu.Game.Online.Multiplayer
// clean up local room state on server disconnect.
if (!connected.NewValue && Room != null)
{
- Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important);
+ Logger.Log("Clearing room due to multiplayer server connection loss.", LoggingTarget.Runtime, LogLevel.Important);
LeaveRoom();
}
}));
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index c061398209..190d150502 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -85,7 +85,13 @@ namespace osu.Game.Online.Multiplayer
catch (HubException exception)
{
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
- connector?.Reconnect();
+ {
+ Debug.Assert(connector != null);
+
+ await connector.Reconnect();
+ return await JoinRoom(roomId, password);
+ }
+
throw;
}
}
diff --git a/osu.Game/Online/Rooms/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs
index 5ae9d58189..afab83b5be 100644
--- a/osu.Game/Online/Rooms/GetRoomsRequest.cs
+++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs
@@ -4,8 +4,8 @@
#nullable disable
using System.Collections.Generic;
-using Humanizer;
using osu.Framework.IO.Network;
+using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
@@ -27,7 +27,7 @@ namespace osu.Game.Online.Rooms
var req = base.CreateWebRequest();
if (status != RoomStatusFilter.Open)
- req.AddParameter("mode", status.ToString().Underscore().ToLowerInvariant());
+ req.AddParameter("mode", status.ToString().ToSnakeCase().ToLowerInvariant());
if (!string.IsNullOrEmpty(category))
req.AddParameter("category", category);
diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
index 2a05fb1bc0..030ca724c4 100644
--- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
+++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
@@ -61,8 +61,15 @@ namespace osu.Game.Online.Spectator
catch (HubException exception)
{
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
- connector?.Reconnect();
- throw;
+ {
+ Debug.Assert(connector != null);
+
+ await connector.Reconnect();
+ await BeginPlayingInternal(state);
+ }
+
+ // Exceptions can occur if, for instance, the locally played beatmap doesn't have a server-side counterpart.
+ // For now, let's ignore these so they don't cause unobserved exceptions to appear to the user (and sentry).
}
}
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 4b5c9c0815..a53ad48a40 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -287,7 +287,9 @@ namespace osu.Game
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
- dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints, beatmapUpdater));
+ dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
+
+ AddInternal(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
BeatmapManager.ProcessBeatmap = set => beatmapUpdater.Process(set);
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index 6acc9bf002..c46c5cde43 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -23,6 +23,7 @@ using osuTK;
using osuTK.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Extensions.LocalisationExtensions;
+using osu.Framework.Graphics.Cursor;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapSet.Scores
@@ -38,8 +39,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private readonly FillFlowContainer backgroundFlow;
- private Color4 highAccuracyColour;
-
public ScoreTable()
{
RelativeSizeAxes = Axes.X;
@@ -57,12 +56,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
});
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- highAccuracyColour = colours.GreenLight;
- }
-
///
/// The statistics that appear in the table, in order of appearance.
///
@@ -158,12 +151,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Current = scoreManager.GetBindableTotalScoreString(score),
Font = OsuFont.GetFont(size: text_size, weight: index == 0 ? FontWeight.Bold : FontWeight.Medium)
},
- new OsuSpriteText
+ new StatisticText(score.Accuracy, 1, showTooltip: false)
{
Margin = new MarginPadding { Right = horizontal_inset },
Text = score.DisplayAccuracy,
- Font = OsuFont.GetFont(size: text_size),
- Colour = score.Accuracy == 1 ? highAccuracyColour : Color4.White
},
new UpdateableFlag(score.User.CountryCode)
{
@@ -171,14 +162,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
ShowPlaceholderOnUnknown = false,
},
username,
- new OsuSpriteText
- {
- Text = score.MaxCombo.ToLocalisableString(@"0\x"),
- Font = OsuFont.GetFont(size: text_size),
#pragma warning disable 618
- Colour = score.MaxCombo == score.BeatmapInfo.MaxCombo ? highAccuracyColour : Color4.White
+ new StatisticText(score.MaxCombo, score.BeatmapInfo.MaxCombo, @"0\x"),
#pragma warning restore 618
- }
};
var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.Result);
@@ -188,23 +174,13 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
if (!availableStatistics.TryGetValue(result.result, out var stat))
stat = new HitResultDisplayStatistic(result.result, 0, null, result.displayName);
- content.Add(new OsuSpriteText
- {
- Text = stat.MaxCount == null ? stat.Count.ToLocalisableString(@"N0") : (LocalisableString)$"{stat.Count}/{stat.MaxCount}",
- Font = OsuFont.GetFont(size: text_size),
- Colour = stat.Count == 0 ? Color4.Gray : Color4.White
- });
+ content.Add(new StatisticText(stat.Count, stat.MaxCount, @"N0") { Colour = stat.Count == 0 ? Color4.Gray : Color4.White });
}
if (showPerformancePoints)
{
Debug.Assert(score.PP != null);
-
- content.Add(new OsuSpriteText
- {
- Text = score.PP.ToLocalisableString(@"N0"),
- Font = OsuFont.GetFont(size: text_size)
- });
+ content.Add(new StatisticText(score.PP.Value, format: @"N0"));
}
content.Add(new ScoreboardTime(score.Date, text_size)
@@ -243,5 +219,31 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Colour = colourProvider.Foreground1;
}
}
+
+ private class StatisticText : OsuSpriteText, IHasTooltip
+ {
+ private readonly double count;
+ private readonly double? maxCount;
+ private readonly bool showTooltip;
+
+ public LocalisableString TooltipText => maxCount == null || !showTooltip ? string.Empty : $"{count}/{maxCount}";
+
+ public StatisticText(double count, double? maxCount = null, string format = null, bool showTooltip = true)
+ {
+ this.count = count;
+ this.maxCount = maxCount;
+ this.showTooltip = showTooltip;
+
+ Text = count.ToLocalisableString(format);
+ Font = OsuFont.GetFont(size: text_size);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ if (count == maxCount)
+ Colour = colours.GreenLight;
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Mods/DifficultyBindable.cs b/osu.Game/Rulesets/Mods/DifficultyBindable.cs
index eb5f97bcf7..34e9fe40a3 100644
--- a/osu.Game/Rulesets/Mods/DifficultyBindable.cs
+++ b/osu.Game/Rulesets/Mods/DifficultyBindable.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@@ -29,7 +27,7 @@ namespace osu.Game.Rulesets.Mods
///
/// A function that can extract the current value of this setting from a beatmap difficulty for display purposes.
///
- public Func ReadCurrentFromDifficulty;
+ public Func? ReadCurrentFromDifficulty;
public float Precision
{
diff --git a/osu.Game/Rulesets/Mods/IApplicableAfterBeatmapConversion.cs b/osu.Game/Rulesets/Mods/IApplicableAfterBeatmapConversion.cs
index 9286f682d1..d45311675d 100644
--- a/osu.Game/Rulesets/Mods/IApplicableAfterBeatmapConversion.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableAfterBeatmapConversion.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
index d0b54f835b..8c99d739cb 100644
--- a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Mods
{
///
diff --git a/osu.Game/Rulesets/Mods/IApplicableMod.cs b/osu.Game/Rulesets/Mods/IApplicableMod.cs
index 7675bd89ef..8ca1a3f8a5 100644
--- a/osu.Game/Rulesets/Mods/IApplicableMod.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableMod.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Mods
{
///
diff --git a/osu.Game/Rulesets/Mods/IApplicableToAudio.cs b/osu.Game/Rulesets/Mods/IApplicableToAudio.cs
index de76790aee..901da7af55 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToAudio.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToAudio.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Mods
{
public interface IApplicableToAudio : IApplicableToTrack, IApplicableToSample
diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs
index 278b4794c5..cff669bf53 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs
index a5ccea1873..8cefb02904 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
index c653a674ef..e23a5d8d99 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs
index 1447511de9..42b520ab26 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs
index f559ed04d7..c8a9ff2f9a 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs
index 8bf2c3810e..7f926dd8b8 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions;
diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableRuleset.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableRuleset.cs
index ace3af62a1..b012beb0c0 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToDrawableRuleset.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableRuleset.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
diff --git a/osu.Game/Rulesets/Mods/IApplicableToHUD.cs b/osu.Game/Rulesets/Mods/IApplicableToHUD.cs
index b5fe299b24..4fb535a0b3 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToHUD.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToHUD.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableToHealthProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableToHealthProcessor.cs
index a58f8640fd..2676060efa 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToHealthProcessor.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToHealthProcessor.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs
index d9fa993393..f7f81c92c0 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableToPlayer.cs b/osu.Game/Rulesets/Mods/IApplicableToPlayer.cs
index c28935607f..bf78428470 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToPlayer.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToPlayer.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableToRate.cs b/osu.Game/Rulesets/Mods/IApplicableToRate.cs
index c66c8f49a1..f613867132 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToRate.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToRate.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Mods
{
///
diff --git a/osu.Game/Rulesets/Mods/IApplicableToSample.cs b/osu.Game/Rulesets/Mods/IApplicableToSample.cs
index 97ed0fbf7e..efd88f2399 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToSample.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToSample.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Audio;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs
index 24c1ac9afe..b93e50921f 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
diff --git a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs
index 358ef71cc0..deecd4bf1f 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Audio;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/ICreateReplay.cs b/osu.Game/Rulesets/Mods/ICreateReplay.cs
index e77f4c49b9..1e5eeca92c 100644
--- a/osu.Game/Rulesets/Mods/ICreateReplay.cs
+++ b/osu.Game/Rulesets/Mods/ICreateReplay.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
diff --git a/osu.Game/Rulesets/Mods/ICreateReplayData.cs b/osu.Game/Rulesets/Mods/ICreateReplayData.cs
index 3ed5c2b7f8..c13e65c7b8 100644
--- a/osu.Game/Rulesets/Mods/ICreateReplayData.cs
+++ b/osu.Game/Rulesets/Mods/ICreateReplayData.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
@@ -45,7 +43,7 @@ namespace osu.Game.Rulesets.Mods
///
public readonly ModCreatedUser User;
- public ModReplayData(Replay replay, ModCreatedUser user = null)
+ public ModReplayData(Replay replay, ModCreatedUser? user = null)
{
Replay = replay;
User = user ?? new ModCreatedUser();
diff --git a/osu.Game/Rulesets/Mods/IHasSeed.cs b/osu.Game/Rulesets/Mods/IHasSeed.cs
index fd2161ac09..001a9d214c 100644
--- a/osu.Game/Rulesets/Mods/IHasSeed.cs
+++ b/osu.Game/Rulesets/Mods/IHasSeed.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs
index 349cc7dd5a..30fa1ea8cb 100644
--- a/osu.Game/Rulesets/Mods/IMod.cs
+++ b/osu.Game/Rulesets/Mods/IMod.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Graphics.Sprites;
diff --git a/osu.Game/Rulesets/Mods/IReadFromConfig.cs b/osu.Game/Rulesets/Mods/IReadFromConfig.cs
index ee6fb6364f..d66fabce70 100644
--- a/osu.Game/Rulesets/Mods/IReadFromConfig.cs
+++ b/osu.Game/Rulesets/Mods/IReadFromConfig.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs
index 3aad858af5..7cf480a11b 100644
--- a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs
+++ b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods
diff --git a/osu.Game/Rulesets/Mods/MetronomeBeat.cs b/osu.Game/Rulesets/Mods/MetronomeBeat.cs
index b26052a37e..149af1e30a 100644
--- a/osu.Game/Rulesets/Mods/MetronomeBeat.cs
+++ b/osu.Game/Rulesets/Mods/MetronomeBeat.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index 17093e3033..abba83ce59 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -117,7 +115,7 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
public virtual Type[] IncompatibleMods => Array.Empty();
- private IReadOnlyList settingsBacking;
+ private IReadOnlyList? settingsBacking;
///
/// A list of the all settings within this mod.
@@ -216,8 +214,8 @@ namespace osu.Game.Rulesets.Mods
public bool Equals(IBindable x, IBindable y)
{
- object xValue = x?.GetUnderlyingSettingValue();
- object yValue = y?.GetUnderlyingSettingValue();
+ object xValue = x.GetUnderlyingSettingValue();
+ object yValue = y.GetUnderlyingSettingValue();
return EqualityComparer
-
+
@@ -84,7 +84,7 @@
-
+
diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index 0794095854..b16e309e52 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -809,6 +809,7 @@ See the LICENCE file in the repository root for full licence text.
True
True
True
+ True
True
True
True