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.Default.Equals(xValue, yValue); } diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index eab0f8dc26..38d26ed05a 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.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; @@ -79,7 +77,7 @@ namespace osu.Game.Rulesets.Mods // Apply a fixed rate change when missing, allowing the player to catch up when the rate is too fast. private const double rate_change_on_miss = 0.95d; - private IAdjustableAudioComponent track; + private IAdjustableAudioComponent? track; private double targetRate = 1d; /// diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index b4ab0500c7..ab49dd5575 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.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.Graphics.Sprites; diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index bd0f2bfe59..bacb953f76 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.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.Framework.Extensions; diff --git a/osu.Game/Rulesets/Mods/ModBlockFail.cs b/osu.Game/Rulesets/Mods/ModBlockFail.cs index a6a8244480..cdfb36ebbc 100644 --- a/osu.Game/Rulesets/Mods/ModBlockFail.cs +++ b/osu.Game/Rulesets/Mods/ModBlockFail.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; using osu.Game.Configuration; using osu.Game.Screens.Play; @@ -11,7 +9,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModBlockFail : Mod, IApplicableFailOverride, IApplicableToHUD, IReadFromConfig { - private Bindable showHealthBar; + private readonly Bindable showHealthBar = new Bindable(); /// /// We never fail, 'yo. @@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Mods public void ReadFromConfig(OsuConfigManager config) { - showHealthBar = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail); + config.BindWith(OsuSetting.ShowHealthDisplayWhenCantFail, showHealthBar); } public void ApplyToHUD(HUDOverlay overlay) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 6e7bd6350e..99c4e71d1f 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.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 osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Rulesets/Mods/ModClassic.cs b/osu.Game/Rulesets/Mods/ModClassic.cs index b4885ff16e..1159955e11 100644 --- a/osu.Game/Rulesets/Mods/ModClassic.cs +++ b/osu.Game/Rulesets/Mods/ModClassic.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.Graphics.Sprites; namespace osu.Game.Rulesets.Mods diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index 262aeb07ac..9e8e44229e 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.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.Bindables; using osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index c0e2c75aca..eefa1531c4 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.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 osu.Framework.Bindables; diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 389d0db261..1c71f5d055 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.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; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index bc5988174b..0f51e2a6d5 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.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; using osu.Game.Beatmaps; diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs index 984892de51..c4396e440e 100644 --- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.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 Humanizer; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -24,7 +22,7 @@ namespace osu.Game.Rulesets.Mods private int retries; - private BindableNumber health; + private readonly BindableNumber health = new BindableDouble(); public override void ApplyToDifficulty(BeatmapDifficulty difficulty) { @@ -46,7 +44,7 @@ namespace osu.Game.Rulesets.Mods public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { - health = healthProcessor.Health.GetBoundCopy(); + health.BindTo(healthProcessor.Health); } } } diff --git a/osu.Game/Rulesets/Mods/ModExtensions.cs b/osu.Game/Rulesets/Mods/ModExtensions.cs index ad61404972..b22030414b 100644 --- a/osu.Game/Rulesets/Mods/ModExtensions.cs +++ b/osu.Game/Rulesets/Mods/ModExtensions.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; diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index e24746ebd9..4425ece513 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.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.Configuration; diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 649d5480bf..e8bc6c2026 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.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.Allocation; @@ -96,13 +94,13 @@ namespace osu.Game.Rulesets.Mods { public readonly BindableInt Combo = new BindableInt(); - private IShader shader; + private IShader shader = null!; protected override DrawNode CreateDrawNode() => new FlashlightDrawNode(this); public override bool RemoveCompletedTransforms => false; - public List Breaks; + public List Breaks = new List(); private readonly float defaultFlashlightSize; private readonly float sizeMultiplier; @@ -207,7 +205,7 @@ namespace osu.Game.Rulesets.Mods { protected new Flashlight Source => (Flashlight)base.Source; - private IShader shader; + private IShader shader = null!; private Quad screenSpaceDrawQuad; private Vector2 flashlightPosition; private Vector2 flashlightSize; @@ -255,7 +253,7 @@ namespace osu.Game.Rulesets.Mods protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - quadBatch?.Dispose(); + quadBatch.Dispose(); } } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 48fa7c13b4..13d89e30d6 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.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; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 030520fccc..0a5348a8cf 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.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; using osu.Game.Beatmaps; diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index 35107762aa..5a8226115f 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.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.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Rulesets/Mods/ModMirror.cs b/osu.Game/Rulesets/Mods/ModMirror.cs index 00a1d4a9c6..3c4b7d0c60 100644 --- a/osu.Game/Rulesets/Mods/ModMirror.cs +++ b/osu.Game/Rulesets/Mods/ModMirror.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 abstract class ModMirror : Mod diff --git a/osu.Game/Rulesets/Mods/ModMuted.cs b/osu.Game/Rulesets/Mods/ModMuted.cs index 88e49f41b0..55d5abfa82 100644 --- a/osu.Game/Rulesets/Mods/ModMuted.cs +++ b/osu.Game/Rulesets/Mods/ModMuted.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 osu.Framework.Audio; using osu.Framework.Bindables; @@ -35,7 +33,7 @@ namespace osu.Game.Rulesets.Mods private readonly BindableNumber mainVolumeAdjust = new BindableDouble(0.5); private readonly BindableNumber metronomeVolumeAdjust = new BindableDouble(0.5); - private BindableNumber currentCombo; + private readonly BindableNumber currentCombo = new BindableInt(); [SettingSource("Enable metronome", "Add a metronome beat to help you keep track of the rhythm.")] public BindableBool EnableMetronome { get; } = new BindableBool @@ -94,7 +92,7 @@ namespace osu.Game.Rulesets.Mods public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { - currentCombo = scoreProcessor.Combo.GetBoundCopy(); + currentCombo.BindTo(scoreProcessor.Combo); currentCombo.BindValueChanged(combo => { double dimFactor = MuteComboCount.Value == 0 ? 1 : (double)combo.NewValue / MuteComboCount.Value; diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 7c2201cd98..c4417ec509 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.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.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -57,10 +55,10 @@ namespace osu.Game.Rulesets.Mods public class NightcoreBeatContainer : BeatSyncedContainer { - private PausableSkinnableSound hatSample; - private PausableSkinnableSound clapSample; - private PausableSkinnableSound kickSample; - private PausableSkinnableSound finishSample; + private PausableSkinnableSound? hatSample; + private PausableSkinnableSound? clapSample; + private PausableSkinnableSound? kickSample; + private PausableSkinnableSound? finishSample; private int? firstBeat; diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index 27dbb53e6c..5ebae17228 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.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; using osu.Game.Graphics; diff --git a/osu.Game/Rulesets/Mods/ModNoMod.cs b/osu.Game/Rulesets/Mods/ModNoMod.cs index cc0b38cbc0..1009c5bc42 100644 --- a/osu.Game/Rulesets/Mods/ModNoMod.cs +++ b/osu.Game/Rulesets/Mods/ModNoMod.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.Graphics.Sprites; namespace osu.Game.Rulesets.Mods diff --git a/osu.Game/Rulesets/Mods/ModNoScope.cs b/osu.Game/Rulesets/Mods/ModNoScope.cs index c43ac33b3f..1b9ce833ad 100644 --- a/osu.Game/Rulesets/Mods/ModNoScope.cs +++ b/osu.Game/Rulesets/Mods/ModNoScope.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.Framework.Graphics; @@ -30,9 +28,9 @@ namespace osu.Game.Rulesets.Mods protected const float TRANSITION_DURATION = 100; - protected BindableNumber CurrentCombo; + protected readonly BindableNumber CurrentCombo = new BindableInt(); - protected IBindable IsBreakTime; + protected readonly IBindable IsBreakTime = new Bindable(); protected float ComboBasedAlpha; @@ -42,14 +40,14 @@ namespace osu.Game.Rulesets.Mods public void ApplyToPlayer(Player player) { - IsBreakTime = player.IsBreakTime.GetBoundCopy(); + IsBreakTime.BindTo(player.IsBreakTime); } public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { if (HiddenComboCount.Value == 0) return; - CurrentCombo = scoreProcessor.Combo.GetBoundCopy(); + CurrentCombo.BindTo(scoreProcessor.Combo); CurrentCombo.BindValueChanged(combo => { ComboBasedAlpha = Math.Max(MIN_ALPHA, 1 - (float)combo.NewValue / HiddenComboCount.Value); diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index b6daf54fa0..9016a24f8d 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.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 osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 6654bff04b..1f7742b075 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.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; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 643280623c..7b55ba4ad0 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.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.Audio; using osu.Framework.Bindables; diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs index 4829d10ddb..e5995ff180 100644 --- a/osu.Game/Rulesets/Mods/ModRelax.cs +++ b/osu.Game/Rulesets/Mods/ModRelax.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; using osu.Game.Graphics; diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 48ab888a90..c8b835f78a 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.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 osu.Framework.Graphics.Sprites; diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index daa4b7c797..7031489d0e 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.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 osu.Framework.Audio; @@ -46,7 +44,7 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - private IAdjustableAudioComponent track; + private IAdjustableAudioComponent? track; protected ModTimeRamp() { diff --git a/osu.Game/Rulesets/Mods/ModType.cs b/osu.Game/Rulesets/Mods/ModType.cs index 5a405b7632..e3c82e42f5 100644 --- a/osu.Game/Rulesets/Mods/ModType.cs +++ b/osu.Game/Rulesets/Mods/ModType.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 enum ModType diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index 2150264a0c..08bd44f7bd 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.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 osu.Framework.Bindables; diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index d9ee561ef8..df8f781148 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.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 osu.Framework.Bindables; diff --git a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs index d84695fff8..2e3619ec63 100644 --- a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs +++ b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.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.Framework.Bindables; using osu.Game.Beatmaps; @@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Mods /// /// The first adjustable object. /// - protected HitObject FirstObject { get; private set; } + protected HitObject? FirstObject { get; private set; } /// /// Whether the visibility of should be increased. @@ -59,7 +57,7 @@ namespace osu.Game.Rulesets.Mods { FirstObject = getFirstAdjustableObjectRecursive(beatmap.HitObjects); - HitObject getFirstAdjustableObjectRecursive(IReadOnlyList hitObjects) + HitObject? getFirstAdjustableObjectRecursive(IReadOnlyList hitObjects) { foreach (var h in hitObjects) { @@ -93,7 +91,7 @@ namespace osu.Game.Rulesets.Mods /// The to check. /// The which may be equal to or contain as a nested object. /// Whether is equal to or nested within . - private bool isObjectEqualToOrNestedIn(HitObject toCheck, HitObject target) + private bool isObjectEqualToOrNestedIn(HitObject toCheck, HitObject? target) { if (target == null) return false; diff --git a/osu.Game/Rulesets/Mods/MultiMod.cs b/osu.Game/Rulesets/Mods/MultiMod.cs index d62dc34d3b..1c41c6b8b3 100644 --- a/osu.Game/Rulesets/Mods/MultiMod.cs +++ b/osu.Game/Rulesets/Mods/MultiMod.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; diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs index e058fba566..72de0ad653 100644 --- a/osu.Game/Rulesets/Mods/UnknownMod.cs +++ b/osu.Game/Rulesets/Mods/UnknownMod.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 class UnknownMod : Mod diff --git a/osu.Game/Rulesets/Objects/HitObjectProperty.cs b/osu.Game/Rulesets/Objects/HitObjectProperty.cs new file mode 100644 index 0000000000..f1df83f80c --- /dev/null +++ b/osu.Game/Rulesets/Objects/HitObjectProperty.cs @@ -0,0 +1,52 @@ +// 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 JetBrains.Annotations; +using osu.Framework.Bindables; + +namespace osu.Game.Rulesets.Objects +{ + /// + /// Represents a wrapper containing a lazily-initialised , backed by a temporary field used for storage until initialisation. + /// + public struct HitObjectProperty + { + [CanBeNull] + private Bindable backingBindable; + + /// + /// A temporary field to store the current value to, prior to 's initialisation. + /// + private T backingValue; + + /// + /// The underlying , only initialised on first access. + /// + public Bindable Bindable => backingBindable ??= new Bindable(defaultValue) { Value = backingValue }; + + /// + /// The current value, derived from and delegated to if initialised, or a temporary field otherwise. + /// + public T Value + { + get => backingBindable != null ? backingBindable.Value : backingValue; + set + { + if (backingBindable != null) + backingBindable.Value = value; + else + backingValue = value; + } + } + + private readonly T defaultValue; + + public HitObjectProperty(T value = default) + { + backingValue = defaultValue = value; + backingBindable = null; + } + } +} diff --git a/osu.Game/Scoring/ScoreModelDownloader.cs b/osu.Game/Scoring/ScoreModelDownloader.cs index 8625c6c5d0..514b7a57de 100644 --- a/osu.Game/Scoring/ScoreModelDownloader.cs +++ b/osu.Game/Scoring/ScoreModelDownloader.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.Extensions; using osu.Game.Online.API; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4eb16a854b..ceadfa1527 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -49,6 +49,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } + [Resolved] + private BeatmapManager beatmapManager { get; set; } + private readonly IBindable isConnected = new Bindable(); private AddItemButton addItemButton; @@ -70,12 +73,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; - isConnected.BindTo(client.IsConnected); - isConnected.BindValueChanged(connected => - { - if (!connected.NewValue) - handleRoomLost(); - }, true); + if (!client.IsConnected.Value) + handleRoomLost(); } protected override Drawable CreateMainContent() => new Container @@ -145,7 +144,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new MultiplayerPlaylist { RelativeSizeAxes = Axes.Both, - RequestEdit = item => OpenSongSelection(item.ID) + RequestEdit = OpenSongSelection } }, new[] @@ -223,12 +222,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer /// Opens the song selection screen to add or edit an item. /// /// An optional playlist item to edit. If null, a new item will be added instead. - internal void OpenSongSelection(long? itemToEdit = null) + internal void OpenSongSelection(PlaylistItem itemToEdit = null) { if (!this.IsCurrentScreen()) return; - this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit)); + int id = itemToEdit?.Beatmap.OnlineID ?? Room.Playlist.Last().Beatmap.OnlineID; + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id); + + var workingBeatmap = localBeatmap == null ? null : beatmapManager.GetWorkingBeatmap(localBeatmap); + + this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit?.ID, workingBeatmap)); } protected override Drawable CreateFooter() => new MultiplayerMatchFooter(); diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index f045ab661e..ee29241321 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Humanizer; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -71,7 +70,7 @@ namespace osu.Game.Screens.Play.HUD var bindable = (IBindable)property.GetValue(component); if (!bindable.IsDefault) - Settings.Add(property.Name.Underscore(), bindable.GetUnderlyingSettingValue()); + Settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue()); } if (component is Container container) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index a6532ee145..50e30c68d5 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -153,17 +153,12 @@ namespace osu.Game.Screens.Select.Carousel { Direction = FillDirection.Horizontal, Spacing = new Vector2(4, 0), + Scale = new Vector2(0.8f), AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new TopLocalRank(beatmapInfo) - { - Scale = new Vector2(0.8f), - }, - starCounter = new StarCounter - { - Scale = new Vector2(0.8f), - } + new TopLocalRank(beatmapInfo), + starCounter = new StarCounter() } } } diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 577b3f5f64..a95d9078a2 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -61,14 +61,25 @@ namespace osu.Game.Screens.Select.Carousel Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Top = 5 }, - Children = new Drawable[] + Spacing = new Vector2(5), + Children = new[] { + beatmapSet.AllBeatmapsUpToDate + ? Empty() + : new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Children = new Drawable[] + { + new UpdateBeatmapSetButton(beatmapSet), + } + }, new BeatmapSetOnlineStatusPill { AutoSizeAxes = Axes.Both, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5 }, TextSize = 11, TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, Status = beatmapSet.Status @@ -76,6 +87,8 @@ namespace osu.Game.Screens.Select.Carousel new FillFlowContainer { AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, Spacing = new Vector2(3), ChildrenEnumerable = getDifficultyIcons(), }, diff --git a/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs b/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs new file mode 100644 index 0000000000..b80eb40018 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs @@ -0,0 +1,138 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Select.Carousel +{ + public class UpdateBeatmapSetButton : OsuAnimatedButton + { + private readonly BeatmapSetInfo beatmapSetInfo; + private SpriteIcon icon = null!; + private Box progressFill = null!; + + public UpdateBeatmapSetButton(BeatmapSetInfo beatmapSetInfo) + { + this.beatmapSetInfo = beatmapSetInfo; + + AutoSizeAxes = Axes.Both; + + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + } + + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + const float icon_size = 14; + + Content.Anchor = Anchor.CentreLeft; + Content.Origin = Anchor.CentreLeft; + + Content.AddRange(new Drawable[] + { + progressFill = new Box + { + Colour = Color4.White, + Alpha = 0.2f, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Width = 0, + }, + new FillFlowContainer + { + Padding = new MarginPadding { Horizontal = 5, Vertical = 3 }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Children = new Drawable[] + { + new Container + { + Size = new Vector2(icon_size), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.SyncAlt, + Size = new Vector2(icon_size), + }, + } + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Text = "Update", + } + } + }, + }); + + Action = () => + { + beatmapDownloader.Download(beatmapSetInfo); + attachExistingDownload(); + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + icon.Spin(4000, RotationDirection.Clockwise); + } + + private void attachExistingDownload() + { + var download = beatmapDownloader.GetExistingDownload(beatmapSetInfo); + + if (download != null) + { + Enabled.Value = false; + TooltipText = string.Empty; + + download.DownloadProgressed += progress => progressFill.ResizeWidthTo(progress, 100, Easing.OutQuint); + download.Failure += _ => attachExistingDownload(); + } + else + { + Enabled.Value = true; + TooltipText = "Update beatmap with online changes"; + + progressFill.ResizeWidthTo(0, 100, Easing.OutQuint); + } + } + + protected override bool OnHover(HoverEvent e) + { + icon.Spin(400, RotationDirection.Clockwise); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + icon.Spin(4000, RotationDirection.Clockwise); + base.OnHoverLost(e); + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6120d3d600..4fa4b804ab 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 8a36ad6e3d..dc012ab2fa 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -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