From 5a1b0004db37148191ccbc349b8bd6fe4ca9e502 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Feb 2024 01:17:28 +0300 Subject: [PATCH 001/106] Add failing test case --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index f97372e9b6ad..b2e607d9a3c5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -24,9 +24,11 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Utils; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -53,6 +55,9 @@ public partial class TestScenePlayerLoader : ScreenTestScene [Cached] private readonly VolumeOverlay volumeOverlay; + [Cached] + private readonly OsuLogo logo; + [Cached(typeof(BatteryInfo))] private readonly LocalBatteryInfo batteryInfo = new LocalBatteryInfo(); @@ -76,7 +81,14 @@ public TestScenePlayerLoader() Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, }, - changelogOverlay = new ChangelogOverlay() + changelogOverlay = new ChangelogOverlay(), + logo = new OsuLogo + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Scale = new Vector2(0.5f), + Position = new Vector2(128f), + }, }); } @@ -204,6 +216,36 @@ public void TestBlockLoadViaFocus() AddUntilStep("loads after idle", () => !loader.IsCurrentScreen()); } + [Test] + public void TestLoadNotBlockedOnOsuLogo() + { + AddStep("load dummy beatmap", () => resetPlayer(false)); + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddUntilStep("wait for load ready", () => + { + moveMouse(); + return player?.LoadState == LoadState.Ready; + }); + + // move mouse in logo while waiting for load to still proceed (it shouldn't be blocked when hovering logo). + AddUntilStep("move mouse in logo", () => + { + moveMouse(); + return !loader.IsCurrentScreen(); + }); + + void moveMouse() + { + notificationOverlay.State.Value = Visibility.Hidden; + + InputManager.MoveMouseTo( + logo.ScreenSpaceDrawQuad.TopLeft + + (logo.ScreenSpaceDrawQuad.BottomRight - logo.ScreenSpaceDrawQuad.TopLeft) + * RNG.NextSingle(0.3f, 0.7f)); + } + } + [Test] public void TestLoadContinuation() { From 0502997ae958e0697fcdbcad2555e80c52b6a22d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Feb 2024 01:02:13 +0300 Subject: [PATCH 002/106] Stop blocking player loader when hovering over osu! logo --- osu.Game/Screens/Play/PlayerLoader.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 232de53ac3cc..201511529ec0 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -106,8 +106,8 @@ protected bool BackgroundBrightnessReduction && ReadyForGameplay; protected virtual bool ReadyForGameplay => - // not ready if the user is hovering one of the panes, unless they are idle. - (IsHovered || idleTracker.IsIdle.Value) + // not ready if the user is hovering one of the panes (logo is excluded), unless they are idle. + (IsHovered || osuLogo?.IsHovered == true || idleTracker.IsIdle.Value) // not ready if the user is dragging a slider or otherwise. && inputManager.DraggedDrawable == null // not ready if a focused overlay is visible, like settings. @@ -306,10 +306,14 @@ public override bool OnExiting(ScreenExitEvent e) return base.OnExiting(e); } + private OsuLogo? osuLogo; + protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); + osuLogo = logo; + const double duration = 300; if (!resuming) logo.MoveTo(new Vector2(0.5f), duration, Easing.OutQuint); @@ -328,6 +332,7 @@ protected override void LogoExiting(OsuLogo logo) { base.LogoExiting(logo); content.StopTracking(); + osuLogo = null; } protected override void LogoSuspending(OsuLogo logo) @@ -338,6 +343,8 @@ protected override void LogoSuspending(OsuLogo logo) logo .FadeOut(CONTENT_OUT_DURATION / 2, Easing.OutQuint) .ScaleTo(logo.Scale * 0.8f, CONTENT_OUT_DURATION * 2, Easing.OutQuint); + + osuLogo = null; } #endregion From 5850d6a57807aee271aae79532964bc85d345e1a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Feb 2024 20:06:51 +0900 Subject: [PATCH 003/106] Show near-misses on the results-screen heatmap --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 7 +- .../Objects/Drawables/DrawableHitCircle.cs | 74 +++++++++++-------- .../Statistics/AccuracyHeatmap.cs | 48 +++++++----- 3 files changed, 75 insertions(+), 54 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 10d7af5e58ac..0e3f972d41c7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -95,12 +95,7 @@ public void ApplyToDrawableHitObject(DrawableHitObject obj) /// private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider) { - var oldHitAction = slider.HitArea.Hit; - slider.HitArea.Hit = () => - { - oldHitAction?.Invoke(); - return !slider.DrawableSlider.AllJudged; - }; + slider.HitArea.CanBeHit = () => !slider.DrawableSlider.AllJudged; } private void applyEarlyFading(DrawableHitCircle circle) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index b1c9bef6c4f5..3727e78d01b5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -10,7 +10,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; @@ -43,7 +42,6 @@ public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircl Drawable IHasApproachCircle.ApproachCircle => ApproachCircle; private Container scaleContainer; - private InputManager inputManager; public DrawableHitCircle() : this(null) @@ -73,14 +71,8 @@ private void load() { HitArea = new HitReceptor { - Hit = () => - { - if (AllJudged) - return false; - - UpdateResult(true); - return true; - }, + CanBeHit = () => !AllJudged, + Hit = () => UpdateResult(true) }, shakeContainer = new ShakeContainer { @@ -114,13 +106,6 @@ private void load() ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - } - public override double LifetimeStart { get => base.LifetimeStart; @@ -155,7 +140,15 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyMinResult(); + { + ApplyResult((r, position) => + { + var circleResult = (OsuHitCircleJudgementResult)r; + + circleResult.Type = r.Judgement.MinResult; + circleResult.CursorPositionAtHit = position; + }, computeHitPosition()); + } return; } @@ -169,22 +162,21 @@ protected override void CheckForResult(bool userTriggered, double timeOffset) if (result == HitResult.None || clickAction != ClickAction.Hit) return; - Vector2? hitPosition = null; - - // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. - if (result.IsHit()) - { - var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); - hitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); - } - ApplyResult<(HitResult result, Vector2? position)>((r, state) => { var circleResult = (OsuHitCircleJudgementResult)r; circleResult.Type = state.result; circleResult.CursorPositionAtHit = state.position; - }, (result, hitPosition)); + }, (result, computeHitPosition())); + } + + private Vector2? computeHitPosition() + { + if (HitArea.ClosestPressPosition is Vector2 screenSpaceHitPosition) + return HitObject.StackedPosition + (ToLocalSpace(screenSpaceHitPosition) - DrawSize / 2); + + return null; } /// @@ -227,6 +219,7 @@ protected override void UpdateHitStateTransforms(ArmedState state) break; case ArmedState.Idle: + HitArea.ClosestPressPosition = null; HitArea.HitAction = null; break; @@ -247,9 +240,11 @@ public partial class HitReceptor : CompositeDrawable, IKeyBindingHandler true; - public Func Hit; + public Func CanBeHit; + public Action Hit; public OsuAction? HitAction; + public Vector2? ClosestPressPosition; public HitReceptor() { @@ -264,12 +259,31 @@ public HitReceptor() public bool OnPressed(KeyBindingPressEvent e) { + if (!(CanBeHit?.Invoke() ?? false)) + return false; + switch (e.Action) { case OsuAction.LeftButton: case OsuAction.RightButton: - if (IsHovered && (Hit?.Invoke() ?? false)) + // Only update closest press position while the object hasn't been hit yet. + if (HitAction == null) + { + if (ClosestPressPosition is Vector2 curClosest) + { + float oldDist = Vector2.DistanceSquared(curClosest, ScreenSpaceDrawQuad.Centre); + float newDist = Vector2.DistanceSquared(e.ScreenSpaceMousePosition, ScreenSpaceDrawQuad.Centre); + + if (newDist < oldDist) + ClosestPressPosition = e.ScreenSpaceMousePosition; + } + else + ClosestPressPosition = e.ScreenSpaceMousePosition; + } + + if (IsHovered) { + Hit?.Invoke(); HitAction ??= e.Action; return true; } diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index f9d4a3b325fe..813f3b2e7a85 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -197,7 +197,9 @@ private void load() var point = new HitPoint(pointType, this) { - BaseColour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) + BaseColour = pointType == HitPointType.Hit + ? new Color4(102, 255, 204, 255) + : new Color4(255, 102, 102, 255) }; points[r][c] = point; @@ -250,12 +252,15 @@ protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radi var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle)); Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2; - float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. + float localRadius = localCentre.X * inner_portion * normalisedDistance; Vector2 localPoint = localCentre + localRadius * rotatedCoordinate; // Find the most relevant hit point. - int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1); - int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1); + int r = (int)Math.Round(localPoint.Y); + int c = (int)Math.Round(localPoint.X); + + if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension) + return; PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment()); @@ -298,28 +303,35 @@ protected override void Update() { base.Update(); - // the point at which alpha is saturated and we begin to adjust colour lightness. - const float lighten_cutoff = 0.95f; + if (pointType == HitPointType.Hit) + { + // the point at which alpha is saturated and we begin to adjust colour lightness. + const float lighten_cutoff = 0.95f; - // the amount of lightness to attribute regardless of relative value to peak point. - const float non_relative_portion = 0.2f; + // the amount of lightness to attribute regardless of relative value to peak point. + const float non_relative_portion = 0.2f; - float amount = 0; + float amount = 0; - // give some amount of alpha regardless of relative count - amount += non_relative_portion * Math.Min(1, count / 10f); + // give some amount of alpha regardless of relative count + amount += non_relative_portion * Math.Min(1, count / 10f); - // add relative portion - amount += (1 - non_relative_portion) * (count / heatmap.PeakValue); + // add relative portion + amount += (1 - non_relative_portion) * (count / heatmap.PeakValue); - // apply easing - amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); + // apply easing + amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); - Debug.Assert(amount <= 1); + Debug.Assert(amount <= 1); - Alpha = Math.Min(amount / lighten_cutoff, 1); - if (pointType == HitPointType.Hit) + Alpha = Math.Min(amount / lighten_cutoff, 1); Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff)); + } + else + { + Alpha = 0.8f; + Colour = BaseColour; + } } } From 99d716f8fc62aaaf09ca65fa7da6f8f876bd6b8a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Feb 2024 22:23:30 +0900 Subject: [PATCH 004/106] Make miss points into crosses --- .../Statistics/AccuracyHeatmap.cs | 121 ++++++++++-------- 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 813f3b2e7a85..2120b929a7ab 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -191,18 +192,22 @@ private void load() for (int c = 0; c < points_per_dimension; c++) { - HitPointType pointType = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius - ? HitPointType.Hit - : HitPointType.Miss; + bool isHit = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius; - var point = new HitPoint(pointType, this) + if (isHit) { - BaseColour = pointType == HitPointType.Hit - ? new Color4(102, 255, 204, 255) - : new Color4(255, 102, 102, 255) - }; - - points[r][c] = point; + points[r][c] = new HitPoint(this) + { + BaseColour = new Color4(102, 255, 204, 255) + }; + } + else + { + points[r][c] = new MissPoint + { + BaseColour = new Color4(255, 102, 102, 255) + }; + } } } @@ -262,83 +267,91 @@ protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radi if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension) return; - PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment()); + PeakValue = Math.Max(PeakValue, ((GridPoint)pointGrid.Content[r][c]).Increment()); bufferedGrid.ForceRedraw(); } - private partial class HitPoint : Circle + private abstract partial class GridPoint : CompositeDrawable { /// /// The base colour which will be lightened/darkened depending on the value of this . /// public Color4 BaseColour; - private readonly HitPointType pointType; - private readonly AccuracyHeatmap heatmap; + public override bool IsPresent => Count > 0; - public override bool IsPresent => count > 0; + protected int Count { get; private set; } - public HitPoint(HitPointType pointType, AccuracyHeatmap heatmap) + /// + /// Increment the value of this point by one. + /// + /// The value after incrementing. + public int Increment() { - this.pointType = pointType; - this.heatmap = heatmap; + return ++Count; + } + } + private partial class MissPoint : GridPoint + { + public MissPoint() + { RelativeSizeAxes = Axes.Both; - Alpha = 1; + + InternalChild = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Times + }; + } + + protected override void Update() + { + Alpha = 0.8f; + Colour = BaseColour; } + } - private int count; + private partial class HitPoint : GridPoint + { + private readonly AccuracyHeatmap heatmap; - /// - /// Increment the value of this point by one. - /// - /// The value after incrementing. - public int Increment() + public HitPoint(AccuracyHeatmap heatmap) { - return ++count; + this.heatmap = heatmap; + + RelativeSizeAxes = Axes.Both; + + InternalChild = new Circle { RelativeSizeAxes = Axes.Both }; } protected override void Update() { base.Update(); - if (pointType == HitPointType.Hit) - { - // the point at which alpha is saturated and we begin to adjust colour lightness. - const float lighten_cutoff = 0.95f; + // the point at which alpha is saturated and we begin to adjust colour lightness. + const float lighten_cutoff = 0.95f; - // the amount of lightness to attribute regardless of relative value to peak point. - const float non_relative_portion = 0.2f; + // the amount of lightness to attribute regardless of relative value to peak point. + const float non_relative_portion = 0.2f; - float amount = 0; + float amount = 0; - // give some amount of alpha regardless of relative count - amount += non_relative_portion * Math.Min(1, count / 10f); + // give some amount of alpha regardless of relative count + amount += non_relative_portion * Math.Min(1, Count / 10f); - // add relative portion - amount += (1 - non_relative_portion) * (count / heatmap.PeakValue); + // add relative portion + amount += (1 - non_relative_portion) * (Count / heatmap.PeakValue); - // apply easing - amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); + // apply easing + amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); - Debug.Assert(amount <= 1); + Debug.Assert(amount <= 1); - Alpha = Math.Min(amount / lighten_cutoff, 1); - Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff)); - } - else - { - Alpha = 0.8f; - Colour = BaseColour; - } + Alpha = Math.Min(amount / lighten_cutoff, 1); + Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff)); } } - - private enum HitPointType - { - Hit, - Miss - } } } From 44b1515cc5395486963615d238f2f77c16b5888c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:28:40 +0900 Subject: [PATCH 005/106] Remove LangVersion redefintions --- osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj | 1 - osu.Game/osu.Game.csproj | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 518ab362ca6b..7817d55f57ce 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -4,7 +4,6 @@ Library true click the circles. to the beat. - 10 diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 935b759e4d25..9bcdebc34796 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -3,7 +3,6 @@ net8.0 Library true - 10 osu! From 9b8f2064867472009e3c5c621c1fbf5823ad7592 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:38:07 +0900 Subject: [PATCH 006/106] Enable NRT for DrawableHitCircle to clean up --- .../Objects/Drawables/DrawableHitCircle.cs | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 3727e78d01b5..b950ef4bbbbb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -1,12 +1,9 @@ // 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.Diagnostics; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -27,34 +24,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircle { - public OsuAction? HitAction => HitArea?.HitAction; + public OsuAction? HitAction => HitArea.HitAction; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; - public SkinnableDrawable ApproachCircle { get; private set; } - public HitReceptor HitArea { get; private set; } - public SkinnableDrawable CirclePiece { get; private set; } + public SkinnableDrawable ApproachCircle { get; private set; } = null!; + public HitReceptor HitArea { get; private set; } = null!; + public SkinnableDrawable CirclePiece { get; private set; } = null!; - protected override IEnumerable DimmablePieces => new[] - { - CirclePiece, - }; + protected override IEnumerable DimmablePieces => new[] { CirclePiece }; Drawable IHasApproachCircle.ApproachCircle => ApproachCircle; - private Container scaleContainer; + private Container scaleContainer = null!; + private ShakeContainer shakeContainer = null!; public DrawableHitCircle() : this(null) { } - public DrawableHitCircle([CanBeNull] HitCircle h = null) + public DrawableHitCircle(HitCircle? h = null) : base(h) { } - private ShakeContainer shakeContainer; - [BackgroundDependencyLoader] private void load() { @@ -219,8 +212,7 @@ protected override void UpdateHitStateTransforms(ArmedState state) break; case ArmedState.Idle: - HitArea.ClosestPressPosition = null; - HitArea.HitAction = null; + HitArea.Reset(); break; case ArmedState.Miss: @@ -240,11 +232,11 @@ public partial class HitReceptor : CompositeDrawable, IKeyBindingHandler true; - public Func CanBeHit; - public Action Hit; + public required Func CanBeHit { get; set; } + public required Action Hit { get; set; } - public OsuAction? HitAction; - public Vector2? ClosestPressPosition; + public OsuAction? HitAction { get; private set; } + public Vector2? ClosestPressPosition { get; private set; } public HitReceptor() { @@ -259,7 +251,7 @@ public HitReceptor() public bool OnPressed(KeyBindingPressEvent e) { - if (!(CanBeHit?.Invoke() ?? false)) + if (CanBeHit()) return false; switch (e.Action) @@ -283,7 +275,7 @@ public bool OnPressed(KeyBindingPressEvent e) if (IsHovered) { - Hit?.Invoke(); + Hit(); HitAction ??= e.Action; return true; } @@ -297,13 +289,19 @@ public bool OnPressed(KeyBindingPressEvent e) public void OnReleased(KeyBindingReleaseEvent e) { } + + public void Reset() + { + HitAction = null; + ClosestPressPosition = null; + } } private partial class ProxyableSkinnableDrawable : SkinnableDrawable { public override bool RemoveWhenNotAlive => false; - public ProxyableSkinnableDrawable(ISkinComponentLookup lookup, Func defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling) + public ProxyableSkinnableDrawable(ISkinComponentLookup lookup, Func? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling) : base(lookup, defaultImplementation, confineMode) { } From 38f7913b31bbc64f99882668b7d8cb89de4d51fd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:50:56 +0900 Subject: [PATCH 007/106] Fix inverted condition --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index b950ef4bbbbb..c9f2983b1e10 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -251,7 +251,7 @@ public HitReceptor() public bool OnPressed(KeyBindingPressEvent e) { - if (CanBeHit()) + if (!CanBeHit()) return false; switch (e.Action) From 2fc06f16b50c0112522b3c2f164de6aa2aa36608 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:51:28 +0900 Subject: [PATCH 008/106] Fix inspections --- osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index c624fbbe7379..9d79cb0db4de 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -91,11 +91,11 @@ public void TestBeatmapColourDefault() var skinnable = firstObject.ApproachCircle; - if (skin == null && skinnable?.Drawable is DefaultApproachCircle) + if (skin == null && skinnable.Drawable is DefaultApproachCircle) // check for default skin provider return true; - var text = skinnable?.Drawable as SpriteText; + var text = skinnable.Drawable as SpriteText; return text?.Text == skin; }); From e2867986c591f042b63c6da36e1f27d40cb89341 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:47:36 +0900 Subject: [PATCH 009/106] Add xmldocs --- .../Objects/Drawables/DrawableHitCircle.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index c9f2983b1e10..f7237d4c032d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -232,10 +232,24 @@ public partial class HitReceptor : CompositeDrawable, IKeyBindingHandler true; + /// + /// Whether the hitobject can still be hit at the current point in time. + /// public required Func CanBeHit { get; set; } + + /// + /// An action that's invoked to perform the hit. + /// public required Action Hit { get; set; } + /// + /// The with which the hit was attempted. + /// public OsuAction? HitAction { get; private set; } + + /// + /// The closest position to the hit receptor at the point where the hit was attempted. + /// public Vector2? ClosestPressPosition { get; private set; } public HitReceptor() @@ -290,6 +304,9 @@ public void OnReleased(KeyBindingReleaseEvent e) { } + /// + /// Resets to a fresh state. + /// public void Reset() { HitAction = null; From 1f13124b3834b846281c5d63acc65275c356258e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Feb 2024 03:52:51 +0900 Subject: [PATCH 010/106] Always process position as long as it's hittable For example... If a hitobject is pressed but the result is a shake, this will stop processing hits. --- .../Objects/Drawables/DrawableHitCircle.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index f7237d4c032d..c3ce6acce9ef 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -272,20 +272,16 @@ public bool OnPressed(KeyBindingPressEvent e) { case OsuAction.LeftButton: case OsuAction.RightButton: - // Only update closest press position while the object hasn't been hit yet. - if (HitAction == null) + if (ClosestPressPosition is Vector2 curClosest) { - if (ClosestPressPosition is Vector2 curClosest) - { - float oldDist = Vector2.DistanceSquared(curClosest, ScreenSpaceDrawQuad.Centre); - float newDist = Vector2.DistanceSquared(e.ScreenSpaceMousePosition, ScreenSpaceDrawQuad.Centre); + float oldDist = Vector2.DistanceSquared(curClosest, ScreenSpaceDrawQuad.Centre); + float newDist = Vector2.DistanceSquared(e.ScreenSpaceMousePosition, ScreenSpaceDrawQuad.Centre); - if (newDist < oldDist) - ClosestPressPosition = e.ScreenSpaceMousePosition; - } - else + if (newDist < oldDist) ClosestPressPosition = e.ScreenSpaceMousePosition; } + else + ClosestPressPosition = e.ScreenSpaceMousePosition; if (IsHovered) { From dcb195f3c813c72d2bd694d1488e0c37c67f1764 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 8 Feb 2024 02:16:08 +0900 Subject: [PATCH 011/106] Add delayed resume for taiko/catch/mania --- .../UI/DrawableCatchRuleset.cs | 3 ++ .../UI/DrawableManiaRuleset.cs | 3 ++ .../UI/DrawableTaikoRuleset.cs | 3 ++ osu.Game/Screens/Play/DelayedResumeOverlay.cs | 32 +++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 osu.Game/Screens/Play/DelayedResumeOverlay.cs diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index f0a327d7acda..580c90bcb494 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Catch.UI { @@ -52,5 +53,7 @@ private void load() protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo); public override DrawableHitObject? CreateDrawableRepresentation(CatchHitObject h) => null; + + protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay(); } } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index decf670c5dea..275b1311dee3 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -26,6 +26,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI @@ -164,6 +165,8 @@ private void updateTimeRange() protected override ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score); + protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay(); + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 77b2b06c0ec8..cd9ed399e6ea 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -101,5 +102,7 @@ public MultiplierControlPoint ControlPointAt(double time) protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); protected override ReplayRecorder CreateReplayRecorder(Score score) => new TaikoReplayRecorder(score); + + protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay(); } } diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs new file mode 100644 index 000000000000..ef39c8eb7684 --- /dev/null +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -0,0 +1,32 @@ +// 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.Localisation; +using osu.Framework.Threading; + +namespace osu.Game.Screens.Play +{ + /// + /// Simple that resumes after 800ms. + /// + public partial class DelayedResumeOverlay : ResumeOverlay + { + protected override LocalisableString Message => string.Empty; + + private ScheduledDelegate? scheduledResume; + + protected override void PopIn() + { + base.PopIn(); + + scheduledResume?.Cancel(); + scheduledResume = Scheduler.AddDelayed(Resume, 800); + } + + protected override void PopOut() + { + base.PopOut(); + scheduledResume?.Cancel(); + } + } +} From f49aa4d8157eb6368263f919c662dd0afdaca1d0 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 28 Feb 2024 22:01:39 +0800 Subject: [PATCH 012/106] Parse points and segments for path string --- .../Objects/Legacy/ConvertHitObjectParser.cs | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index f9e32fe26f82..30e4101a8468 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -266,30 +266,49 @@ private PathControlPoint[] convertPathString(string pointString, Vector2 offset) // This code takes on the responsibility of handling explicit segments of the path ("X" & "Y" from above). Implicit segments are handled by calls to convertPoints(). string[] pointSplit = pointString.Split('|'); - var controlPoints = new List>(); - int startIndex = 0; - int endIndex = 0; - bool first = true; + Span points = stackalloc Vector2[pointSplit.Length]; + Span<(PathType Type, int StartIndex)> segments = stackalloc (PathType Type, int StartIndex)[pointSplit.Length]; + int pointsCount = 0; + int segmentsCount = 0; - while (++endIndex < pointSplit.Length) + foreach (string s in pointSplit) { - // Keep incrementing endIndex while it's not the start of a new segment (indicated by having an alpha character at position 0). - if (!char.IsLetter(pointSplit[endIndex][0])) - continue; + if (char.IsLetter(s[0])) + { + // The start of a new segment(indicated by having an alpha character at position 0). + var pathType = convertPathType(s); + segments[segmentsCount++] = (pathType, pointsCount); + } + else + { + points[pointsCount++] = readPoint(s, offset); + } + } - // Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment. - // The start of the next segment is the index after the type descriptor. - string endPoint = endIndex < pointSplit.Length - 1 ? pointSplit[endIndex + 1] : null; + var controlPoints = new List(pointsCount); - controlPoints.AddRange(convertPoints(pointSplit.AsMemory().Slice(startIndex, endIndex - startIndex), endPoint, first, offset)); - startIndex = endIndex; - first = false; + for (int i = 0; i < segmentsCount; i++) + { + int startIndex = segments[i].StartIndex; + int endIndex = i < segmentsCount - 1 ? segments[i + 1].StartIndex : pointsCount; + Vector2? endPoint = i < segmentsCount - 1 ? points[endIndex] : null; + controlPoints.AddRange(convertPoints(segments[i].Type, points[startIndex..endIndex], endPoint)); } - if (endIndex > startIndex) - controlPoints.AddRange(convertPoints(pointSplit.AsMemory().Slice(startIndex, endIndex - startIndex), null, first, offset)); + return controlPoints.ToArray(); - return mergePointsLists(controlPoints); + static Vector2 readPoint(string value, Vector2 startPos) + { + string[] vertexSplit = value.Split(':'); + + Vector2 pos = new Vector2((int)Parsing.ParseDouble(vertexSplit[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(vertexSplit[1], Parsing.MAX_COORDINATE_VALUE)) - startPos; + return pos; + } + } + + private IEnumerable convertPoints(PathType type, ReadOnlySpan points, Vector2? endPoint) + { + throw new NotImplementedException(); } /// From 4bff54d35dbd60aab7cd8a255977fe95e5c3bde7 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 28 Feb 2024 22:37:14 +0800 Subject: [PATCH 013/106] Add ToString on PathControlPoint for debugging --- osu.Game/Rulesets/Objects/PathControlPoint.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/Objects/PathControlPoint.cs b/osu.Game/Rulesets/Objects/PathControlPoint.cs index ae9fa0808513..1f8e63b26952 100644 --- a/osu.Game/Rulesets/Objects/PathControlPoint.cs +++ b/osu.Game/Rulesets/Objects/PathControlPoint.cs @@ -76,5 +76,9 @@ public PathControlPoint(Vector2 position, PathType? type = null) } public bool Equals(PathControlPoint other) => Position == other?.Position && Type == other.Type; + + public override string ToString() => type is null + ? $"Position={Position}" + : $"Position={Position}, Type={type}"; } } From fe34577ee2938c45d295800405a309ad3d55ac3e Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 28 Feb 2024 22:42:08 +0800 Subject: [PATCH 014/106] Update parsing. --- .../Objects/Legacy/ConvertHitObjectParser.cs | 86 ++++++------------- 1 file changed, 24 insertions(+), 62 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 30e4101a8468..2d4d2865e6ff 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -278,6 +278,10 @@ private PathControlPoint[] convertPathString(string pointString, Vector2 offset) // The start of a new segment(indicated by having an alpha character at position 0). var pathType = convertPathType(s); segments[segmentsCount++] = (pathType, pointsCount); + + // First segment is prepended by an extra zero point + if (pointsCount == 0) + points[pointsCount++] = Vector2.Zero; } else { @@ -306,47 +310,30 @@ static Vector2 readPoint(string value, Vector2 startPos) } } - private IEnumerable convertPoints(PathType type, ReadOnlySpan points, Vector2? endPoint) - { - throw new NotImplementedException(); - } - /// /// Converts a given point list into a set of path segments. /// + /// The path type of the point list. /// The point list. /// Any extra endpoint to consider as part of the points. This will NOT be returned. - /// Whether this is the first segment in the set. If true the first of the returned segments will contain a zero point. - /// The positional offset to apply to the control points. - /// The set of points contained by as one or more segments of the path, prepended by an extra zero point if is true. - private IEnumerable> convertPoints(ReadOnlyMemory points, string endPoint, bool first, Vector2 offset) + /// The set of points contained by as one or more segments of the path. + private IEnumerable convertPoints(PathType type, ReadOnlySpan points, Vector2? endPoint) { - PathType type = convertPathType(points.Span[0]); - - int readOffset = first ? 1 : 0; // First control point is zero for the first segment. - int readablePoints = points.Length - 1; // Total points readable from the base point span. - int endPointLength = endPoint != null ? 1 : 0; // Extra length if an endpoint is given that lies outside the base point span. - - var vertices = new PathControlPoint[readOffset + readablePoints + endPointLength]; - - // Fill any non-read points. - for (int i = 0; i < readOffset; i++) - vertices[i] = new PathControlPoint(); + var vertices = new PathControlPoint[points.Length]; + var result = new List(); // Parse into control points. - for (int i = 1; i < points.Length; i++) - readPoint(points.Span[i], offset, out vertices[readOffset + i - 1]); - - // If an endpoint is given, add it to the end. - if (endPoint != null) - readPoint(endPoint, offset, out vertices[^1]); + for (int i = 0; i < points.Length; i++) + vertices[i] = new PathControlPoint { Position = points[i] }; // Edge-case rules (to match stable). if (type == PathType.PERFECT_CURVE) { - if (vertices.Length != 3) + int endPointLength = endPoint is null ? 0 : 1; + + if (vertices.Length + endPointLength != 3) type = PathType.BEZIER; - else if (isLinear(vertices)) + else if (isLinear(stackalloc[] { points[0], points[1], endPoint ?? points[2] })) { // osu-stable special-cased colinear perfect curves to a linear path type = PathType.LINEAR; @@ -365,7 +352,7 @@ private IEnumerable> convertPoints(ReadOnlyMemory> convertPoints(ReadOnlyMemory startIndex) - yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex); + for (int i = startIndex; i < endIndex; i++) + result.Add(vertices[i]); - static void readPoint(string value, Vector2 startPos, out PathControlPoint point) - { - string[] vertexSplit = value.Split(':'); - - Vector2 pos = new Vector2((int)Parsing.ParseDouble(vertexSplit[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(vertexSplit[1], Parsing.MAX_COORDINATE_VALUE)) - startPos; - point = new PathControlPoint { Position = pos }; - } - - static bool isLinear(PathControlPoint[] p) => Precision.AlmostEquals(0, (p[1].Position.Y - p[0].Position.Y) * (p[2].Position.X - p[0].Position.X) - - (p[1].Position.X - p[0].Position.X) * (p[2].Position.Y - p[0].Position.Y)); - } - - private PathControlPoint[] mergePointsLists(List> controlPointList) - { - int totalCount = 0; - - foreach (var arr in controlPointList) - totalCount += arr.Length; - - var mergedArray = new PathControlPoint[totalCount]; - var mergedArrayMemory = mergedArray.AsMemory(); - int copyIndex = 0; - - foreach (var arr in controlPointList) - { - arr.CopyTo(mergedArrayMemory.Slice(copyIndex)); - copyIndex += arr.Length; - } + return result; - return mergedArray; + static bool isLinear(ReadOnlySpan p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) + - (p[1].X - p[0].X) * (p[2].Y - p[0].Y)); } /// From bcb91f348d746bd4470240c23c72ec812fc54113 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 28 Feb 2024 22:51:36 +0800 Subject: [PATCH 015/106] Use ArrayPool instead of stackalloc --- .../Objects/Legacy/ConvertHitObjectParser.cs | 78 ++++++++++--------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 2d4d2865e6ff..a3ca719ff998 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -18,6 +18,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Skinning; using osu.Game.Utils; +using System.Buffers; namespace osu.Game.Rulesets.Objects.Legacy { @@ -266,41 +267,49 @@ private PathControlPoint[] convertPathString(string pointString, Vector2 offset) // This code takes on the responsibility of handling explicit segments of the path ("X" & "Y" from above). Implicit segments are handled by calls to convertPoints(). string[] pointSplit = pointString.Split('|'); - Span points = stackalloc Vector2[pointSplit.Length]; - Span<(PathType Type, int StartIndex)> segments = stackalloc (PathType Type, int StartIndex)[pointSplit.Length]; + var points = ArrayPool.Shared.Rent(pointSplit.Length); + var segments = ArrayPool<(PathType Type, int StartIndex)>.Shared.Rent(pointSplit.Length); int pointsCount = 0; int segmentsCount = 0; - - foreach (string s in pointSplit) + try { - if (char.IsLetter(s[0])) + + foreach (string s in pointSplit) { - // The start of a new segment(indicated by having an alpha character at position 0). - var pathType = convertPathType(s); - segments[segmentsCount++] = (pathType, pointsCount); + if (char.IsLetter(s[0])) + { + // The start of a new segment(indicated by having an alpha character at position 0). + var pathType = convertPathType(s); + segments[segmentsCount++] = (pathType, pointsCount); - // First segment is prepended by an extra zero point - if (pointsCount == 0) - points[pointsCount++] = Vector2.Zero; + // First segment is prepended by an extra zero point + if (pointsCount == 0) + points[pointsCount++] = Vector2.Zero; + } + else + { + points[pointsCount++] = readPoint(s, offset); + } } - else + + var controlPoints = new List>(pointsCount); + + for (int i = 0; i < segmentsCount; i++) { - points[pointsCount++] = readPoint(s, offset); + int startIndex = segments[i].StartIndex; + int endIndex = i < segmentsCount - 1 ? segments[i + 1].StartIndex : pointsCount; + Vector2? endPoint = i < segmentsCount - 1 ? points[endIndex] : null; + controlPoints.AddRange(convertPoints(segments[i].Type, new ArraySegment(points, startIndex, endIndex - startIndex), endPoint)); } - } - var controlPoints = new List(pointsCount); - - for (int i = 0; i < segmentsCount; i++) + return controlPoints.SelectMany(s => s).ToArray(); + } + finally { - int startIndex = segments[i].StartIndex; - int endIndex = i < segmentsCount - 1 ? segments[i + 1].StartIndex : pointsCount; - Vector2? endPoint = i < segmentsCount - 1 ? points[endIndex] : null; - controlPoints.AddRange(convertPoints(segments[i].Type, points[startIndex..endIndex], endPoint)); + ArrayPool.Shared.Return(points); + ArrayPool<(PathType Type, int StartIndex)>.Shared.Return(segments); } - return controlPoints.ToArray(); - static Vector2 readPoint(string value, Vector2 startPos) { string[] vertexSplit = value.Split(':'); @@ -317,13 +326,12 @@ static Vector2 readPoint(string value, Vector2 startPos) /// The point list. /// Any extra endpoint to consider as part of the points. This will NOT be returned. /// The set of points contained by as one or more segments of the path. - private IEnumerable convertPoints(PathType type, ReadOnlySpan points, Vector2? endPoint) + private IEnumerable> convertPoints(PathType type, ArraySegment points, Vector2? endPoint) { - var vertices = new PathControlPoint[points.Length]; - var result = new List(); + var vertices = new PathControlPoint[points.Count]; // Parse into control points. - for (int i = 0; i < points.Length; i++) + for (int i = 0; i < points.Count; i++) vertices[i] = new PathControlPoint { Position = points[i] }; // Edge-case rules (to match stable). @@ -333,7 +341,7 @@ private IEnumerable convertPoints(PathType type, ReadOnlySpan< if (vertices.Length + endPointLength != 3) type = PathType.BEZIER; - else if (isLinear(stackalloc[] { points[0], points[1], endPoint ?? points[2] })) + else if (isLinear(points[0], points[1], endPoint ?? points[2])) { // osu-stable special-cased colinear perfect curves to a linear path type = PathType.LINEAR; @@ -370,20 +378,18 @@ private IEnumerable convertPoints(PathType type, ReadOnlySpan< // Force a type on the last point, and return the current control point set as a segment. vertices[endIndex - 1].Type = type; - for (int i = startIndex; i < endIndex; i++) - result.Add(vertices[i]); + yield return new ArraySegment(vertices, startIndex, endIndex - startIndex); // Skip the current control point - as it's the same as the one that's just been returned. startIndex = endIndex + 1; } - for (int i = startIndex; i < endIndex; i++) - result.Add(vertices[i]); - - return result; + if (startIndex < endIndex) + yield return new ArraySegment(vertices, startIndex, endIndex - startIndex); - static bool isLinear(ReadOnlySpan p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - - (p[1].X - p[0].X) * (p[2].Y - p[0].Y)); + static bool isLinear(Vector2 p0, Vector2 p1, Vector2 p2) + => Precision.AlmostEquals(0, (p1.Y - p0.Y) * (p2.X - p0.X) + - (p1.X - p0.X) * (p2.Y - p0.Y)); } /// From 470d2be2e1b4043953e067ef91e4c997138cc0ef Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 29 Feb 2024 00:07:00 +0800 Subject: [PATCH 016/106] The overhead of LINQ is not ignorable --- .../Objects/Legacy/ConvertHitObjectParser.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index a3ca719ff998..72053648a074 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -302,7 +302,7 @@ private PathControlPoint[] convertPathString(string pointString, Vector2 offset) controlPoints.AddRange(convertPoints(segments[i].Type, new ArraySegment(points, startIndex, endIndex - startIndex), endPoint)); } - return controlPoints.SelectMany(s => s).ToArray(); + return mergePointsLists(controlPoints); } finally { @@ -392,6 +392,25 @@ static bool isLinear(Vector2 p0, Vector2 p1, Vector2 p2) - (p1.X - p0.X) * (p2.Y - p0.Y)); } + private PathControlPoint[] mergePointsLists(List> controlPointList) + { + int totalCount = 0; + + foreach (var arr in controlPointList) + totalCount += arr.Count; + + var mergedArray = new PathControlPoint[totalCount]; + int copyIndex = 0; + + foreach (var arr in controlPointList) + { + arr.AsSpan().CopyTo(mergedArray.AsSpan(copyIndex)); + copyIndex += arr.Count; + } + + return mergedArray; + } + /// /// Creates a legacy Hit-type hit object. /// From e86ebd6cdba85e506a771aeab0fdeb0a71d74fab Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 29 Feb 2024 00:24:24 +0800 Subject: [PATCH 017/106] Fix formatting --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 72053648a074..f042e6ba2623 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -271,9 +271,9 @@ private PathControlPoint[] convertPathString(string pointString, Vector2 offset) var segments = ArrayPool<(PathType Type, int StartIndex)>.Shared.Rent(pointSplit.Length); int pointsCount = 0; int segmentsCount = 0; + try { - foreach (string s in pointSplit) { if (char.IsLetter(s[0])) From f28f19ed7eeee1ecacc909f0f4f35edb07c7b8cb Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 29 Feb 2024 10:47:16 +0800 Subject: [PATCH 018/106] Fix method indent size Co-authored-by: Salman Ahmed --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index f042e6ba2623..db1fbe3fa4f3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -389,7 +389,7 @@ private IEnumerable> convertPoints(PathType type, static bool isLinear(Vector2 p0, Vector2 p1, Vector2 p2) => Precision.AlmostEquals(0, (p1.Y - p0.Y) * (p2.X - p0.X) - - (p1.X - p0.X) * (p2.Y - p0.Y)); + - (p1.X - p0.X) * (p2.Y - p0.Y)); } private PathControlPoint[] mergePointsLists(List> controlPointList) From a11e63b184acc5030e3d167f13d85a3691f0b7dc Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 29 Feb 2024 20:02:04 +0800 Subject: [PATCH 019/106] Make the code more clear --- .../Objects/Legacy/ConvertHitObjectParser.cs | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index db1fbe3fa4f3..2b058d5e1f89 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -265,49 +265,59 @@ private PathType convertPathType(string input) private PathControlPoint[] convertPathString(string pointString, Vector2 offset) { // This code takes on the responsibility of handling explicit segments of the path ("X" & "Y" from above). Implicit segments are handled by calls to convertPoints(). - string[] pointSplit = pointString.Split('|'); + string[] pointStringSplit = pointString.Split('|'); - var points = ArrayPool.Shared.Rent(pointSplit.Length); - var segments = ArrayPool<(PathType Type, int StartIndex)>.Shared.Rent(pointSplit.Length); - int pointsCount = 0; - int segmentsCount = 0; + var pointsBuffer = ArrayPool.Shared.Rent(pointStringSplit.Length); + var segmentsBuffer = ArrayPool<(PathType Type, int StartIndex)>.Shared.Rent(pointStringSplit.Length); + int currentPointsIndex = 0; + int currentSegmentsIndex = 0; try { - foreach (string s in pointSplit) + foreach (string s in pointStringSplit) { if (char.IsLetter(s[0])) { // The start of a new segment(indicated by having an alpha character at position 0). var pathType = convertPathType(s); - segments[segmentsCount++] = (pathType, pointsCount); + segmentsBuffer[currentSegmentsIndex++] = (pathType, currentPointsIndex); // First segment is prepended by an extra zero point - if (pointsCount == 0) - points[pointsCount++] = Vector2.Zero; + if (currentPointsIndex == 0) + pointsBuffer[currentPointsIndex++] = Vector2.Zero; } else { - points[pointsCount++] = readPoint(s, offset); + pointsBuffer[currentPointsIndex++] = readPoint(s, offset); } } + int pointsCount = currentPointsIndex; + int segmentsCount = currentSegmentsIndex; var controlPoints = new List>(pointsCount); + var allPoints = new ArraySegment(pointsBuffer, 0, pointsCount); for (int i = 0; i < segmentsCount; i++) { - int startIndex = segments[i].StartIndex; - int endIndex = i < segmentsCount - 1 ? segments[i + 1].StartIndex : pointsCount; - Vector2? endPoint = i < segmentsCount - 1 ? points[endIndex] : null; - controlPoints.AddRange(convertPoints(segments[i].Type, new ArraySegment(points, startIndex, endIndex - startIndex), endPoint)); + if (i < segmentsCount - 1) + { + int startIndex = segmentsBuffer[i].StartIndex; + int endIndex = segmentsBuffer[i + 1].StartIndex; + controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints[startIndex..endIndex], pointsBuffer[endIndex])); + } + else + { + int startIndex = segmentsBuffer[i].StartIndex; + controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints[startIndex..], null)); + } } return mergePointsLists(controlPoints); } finally { - ArrayPool.Shared.Return(points); - ArrayPool<(PathType Type, int StartIndex)>.Shared.Return(segments); + ArrayPool.Shared.Return(pointsBuffer); + ArrayPool<(PathType, int)>.Shared.Return(segmentsBuffer); } static Vector2 readPoint(string value, Vector2 startPos) From 060e17e9898256128632ab1a524ab33c87d0b9c2 Mon Sep 17 00:00:00 2001 From: jvyden Date: Thu, 29 Feb 2024 19:57:32 -0500 Subject: [PATCH 020/106] Support Discord game invites in multiplayer lobbies --- osu.Desktop/DiscordRichPresence.cs | 83 +++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index f0da70876689..b85abdb4fec0 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -9,10 +9,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; +using osu.Game; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Users; using LogLevel = osu.Framework.Logging.LogLevel; @@ -22,6 +25,7 @@ namespace osu.Desktop internal partial class DiscordRichPresence : Component { private const string client_id = "367827983903490050"; + public const string DISCORD_PROTOCOL = $"discord-{client_id}://"; private DiscordRpcClient client = null!; @@ -33,6 +37,12 @@ internal partial class DiscordRichPresence : Component [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private OsuGame game { get; set; } = null!; + + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } = null!; + private readonly IBindable status = new Bindable(); private readonly IBindable activity = new Bindable(); @@ -40,7 +50,12 @@ internal partial class DiscordRichPresence : Component private readonly RichPresence presence = new RichPresence { - Assets = new Assets { LargeImageKey = "osu_logo_lazer", } + Assets = new Assets { LargeImageKey = "osu_logo_lazer" }, + Secrets = new Secrets + { + JoinSecret = null, + SpectateSecret = null, + }, }; [BackgroundDependencyLoader] @@ -52,8 +67,14 @@ private void load(OsuConfigManager config) }; client.OnReady += onReady; + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error); - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); + // set up stuff for spectate/join + // first, we register a uri scheme for when osu! isn't running and a user clicks join/spectate + // the rpc library we use also happens to _require_ that we do this + client.RegisterUriScheme(); + client.Subscribe(EventType.Join); // we have to explicitly tell discord to send us join events. + client.OnJoin += onJoin; config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); @@ -114,6 +135,28 @@ private void updateStatus() { presence.Buttons = null; } + + if (!hideIdentifiableInformation && multiplayerClient.Room != null) + { + MultiplayerRoom room = multiplayerClient.Room; + presence.Party = new Party + { + Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private, + ID = room.RoomID.ToString(), + // technically lobbies can have infinite users, but Discord needs this to be set to something. + // 1024 just happens to look nice. + // https://discord.com/channels/188630481301012481/188630652340404224/1212967974793642034 + Max = 1024, + Size = room.Users.Count, + }; + + presence.Secrets.JoinSecret = $"{room.RoomID}:{room.Settings.Password}"; + } + else + { + presence.Party = null; + presence.Secrets.JoinSecret = null; + } } else { @@ -139,6 +182,22 @@ private void updateStatus() client.SetPresence(presence); } + private void onJoin(object sender, JoinMessage args) + { + game.Window?.Raise(); // users will expect to be brought back to osu! when joining a lobby from discord + + if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) + Logger.Log("Failed to parse the room secret Discord gave us", LoggingTarget.Network, LogLevel.Error); + + var request = new GetRoomRequest(roomId); + request.Success += room => Schedule(() => + { + game.PresentMultiplayerMatch(room, password); + }); + request.Failure += _ => Logger.Log("Couldn't find the room Discord gave us", LoggingTarget.Network, LogLevel.Error); + api.Queue(request); + } + private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' }); private string truncate(string str) @@ -160,6 +219,26 @@ private string truncate(string str) }); } + private static bool tryParseRoomSecret(ReadOnlySpan secret, out long roomId, out string? password) + { + roomId = 0; + password = null; + + int roomSecretSplitIndex = secret.IndexOf(':'); + + if (roomSecretSplitIndex == -1) + return false; + + if (!long.TryParse(secret[..roomSecretSplitIndex], out roomId)) + return false; + + // just convert to string here, we're going to have to alloc it later anyways + password = secret[(roomSecretSplitIndex + 1)..].ToString(); + if (password.Length == 0) password = null; + + return true; + } + private int? getBeatmapID(UserActivity activity) { switch (activity) From 92235e7789271da1080f6f8f635e52f5f8490002 Mon Sep 17 00:00:00 2001 From: jvyden Date: Fri, 1 Mar 2024 00:02:20 -0500 Subject: [PATCH 021/106] Make truncate and getBeatmapID static Code quality was complaining about hidden variables so I opted for this solution. --- osu.Desktop/DiscordRichPresence.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index b85abdb4fec0..91f7f6e1da1a 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -200,7 +200,7 @@ private void onJoin(object sender, JoinMessage args) private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' }); - private string truncate(string str) + private static string truncate(string str) { if (Encoding.UTF8.GetByteCount(str) <= 128) return str; @@ -239,7 +239,7 @@ private static bool tryParseRoomSecret(ReadOnlySpan secret, out long roomI return true; } - private int? getBeatmapID(UserActivity activity) + private static int? getBeatmapID(UserActivity activity) { switch (activity) { From 37e7a4dea7f957231a4eb2aaf9e95654ab4d711e Mon Sep 17 00:00:00 2001 From: Jayden Date: Fri, 1 Mar 2024 14:32:44 -0500 Subject: [PATCH 022/106] Fix yapping Co-authored-by: Salman Ahmed --- osu.Desktop/DiscordRichPresence.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 91f7f6e1da1a..035add8044d9 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -25,7 +25,6 @@ namespace osu.Desktop internal partial class DiscordRichPresence : Component { private const string client_id = "367827983903490050"; - public const string DISCORD_PROTOCOL = $"discord-{client_id}://"; private DiscordRpcClient client = null!; @@ -69,11 +68,9 @@ private void load(OsuConfigManager config) client.OnReady += onReady; client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error); - // set up stuff for spectate/join - // first, we register a uri scheme for when osu! isn't running and a user clicks join/spectate - // the rpc library we use also happens to _require_ that we do this + // A URI scheme is required to support game invitations, as well as informing Discord of the game executable path to support launching the game when a user clicks on join/spectate. client.RegisterUriScheme(); - client.Subscribe(EventType.Join); // we have to explicitly tell discord to send us join events. + client.Subscribe(EventType.Join); client.OnJoin += onJoin; config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); @@ -184,7 +181,7 @@ private void updateStatus() private void onJoin(object sender, JoinMessage args) { - game.Window?.Raise(); // users will expect to be brought back to osu! when joining a lobby from discord + game.Window?.Raise(); if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) Logger.Log("Failed to parse the room secret Discord gave us", LoggingTarget.Network, LogLevel.Error); @@ -232,7 +229,6 @@ private static bool tryParseRoomSecret(ReadOnlySpan secret, out long roomI if (!long.TryParse(secret[..roomSecretSplitIndex], out roomId)) return false; - // just convert to string here, we're going to have to alloc it later anyways password = secret[(roomSecretSplitIndex + 1)..].ToString(); if (password.Length == 0) password = null; From bce3bd55e5a863e52f41598c306a248a79638843 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Mar 2024 16:08:17 +0900 Subject: [PATCH 023/106] Fix catch by moving cursor-specific handling local --- .../TestSceneResumeOverlay.cs | 16 +++++++++++++--- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 7 +++++++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- osu.Game/Screens/Play/ResumeOverlay.cs | 3 ++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index 25d0b0a3d34e..b35984a2fc37 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -6,11 +6,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Tests.Gameplay; using osu.Game.Tests.Visual; @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Tests public partial class TestSceneResumeOverlay : OsuManualInputManagerTestScene { private ManualOsuInputManager osuInputManager = null!; - private CursorContainer cursor = null!; + private GameplayCursorContainer cursor = null!; private ResumeOverlay resume = null!; private bool resumeFired; @@ -99,7 +99,17 @@ public void TestResume(float cursorSize) private void loadContent() { - Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) { Children = new Drawable[] { cursor = new CursorContainer(), resume = new OsuResumeOverlay { GameplayCursor = cursor }, } }; + Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) + { + Children = new Drawable[] + { + cursor = new GameplayCursorContainer(), + resume = new OsuResumeOverlay + { + GameplayCursor = cursor + }, + } + }; resumeFired = false; resume.ResumeAction = () => resumeFired = true; diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 8a84fe14e52a..adc7bd97ff8c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -39,6 +39,13 @@ private void load() protected override void PopIn() { + // Can't display if the cursor is outside the window. + if (GameplayCursor.LastFrameState == Visibility.Hidden || !Contains(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre)) + { + Resume(); + return; + } + base.PopIn(); GameplayCursor.ActiveCursor.Hide(); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 4aeb3d4862ac..218fdf5b86b1 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -227,7 +227,7 @@ private void loadObjects(CancellationToken cancellationToken) public override void RequestResume(Action continueResume) { - if (ResumeOverlay != null && UseResumeOverlay && (Cursor == null || (Cursor.LastFrameState == Visibility.Visible && Contains(Cursor.ActiveCursor.ScreenSpaceDrawQuad.Centre)))) + if (ResumeOverlay != null && UseResumeOverlay) { ResumeOverlay.GameplayCursor = Cursor; ResumeOverlay.ResumeAction = continueResume; diff --git a/osu.Game/Screens/Play/ResumeOverlay.cs b/osu.Game/Screens/Play/ResumeOverlay.cs index fae406bd6bfe..a33dd7988859 100644 --- a/osu.Game/Screens/Play/ResumeOverlay.cs +++ b/osu.Game/Screens/Play/ResumeOverlay.cs @@ -11,6 +11,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.UI; using osuTK; using osuTK.Graphics; @@ -21,7 +22,7 @@ namespace osu.Game.Screens.Play /// public abstract partial class ResumeOverlay : VisibilityContainer { - public CursorContainer GameplayCursor { get; set; } + public GameplayCursorContainer GameplayCursor { get; set; } /// /// The action to be performed to complete resuming. From 6635d9be04952b43b41ff8e3f2596999a069ff74 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Mar 2024 16:08:25 +0900 Subject: [PATCH 024/106] Add countdown display --- .../UI/DrawableCatchRuleset.cs | 3 +- .../Gameplay/TestSceneDelayedResumeOverlay.cs | 44 ++++++ osu.Game/Screens/Play/DelayedResumeOverlay.cs | 135 +++++++++++++++++- 3 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneDelayedResumeOverlay.cs diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 580c90bcb494..32ebdc11598e 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osuTK; namespace osu.Game.Rulesets.Catch.UI { @@ -54,6 +55,6 @@ private void load() public override DrawableHitObject? CreateDrawableRepresentation(CatchHitObject h) => null; - protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay(); + protected override ResumeOverlay CreateResumeOverlay() => new DelayedResumeOverlay { Scale = new Vector2(0.65f) }; } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDelayedResumeOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDelayedResumeOverlay.cs new file mode 100644 index 000000000000..241a78b6b848 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDelayedResumeOverlay.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; +using osu.Game.Tests.Gameplay; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneDelayedResumeOverlay : OsuTestScene + { + private ResumeOverlay resume = null!; + private bool resumeFired; + + [Cached] + private GameplayState gameplayState; + + public TestSceneDelayedResumeOverlay() + { + gameplayState = TestGameplayState.Create(new OsuRuleset()); + } + + [SetUp] + public void SetUp() => Schedule(loadContent); + + [Test] + public void TestResume() + { + AddStep("show", () => resume.Show()); + AddUntilStep("dismissed", () => resumeFired && resume.State.Value == Visibility.Hidden); + } + + private void loadContent() + { + Child = resume = new DelayedResumeOverlay(); + + resumeFired = false; + resume.ResumeAction = () => resumeFired = true; + } + } +} diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index ef39c8eb7684..6f70a914f043 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -1,8 +1,18 @@ // 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 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.Localisation; using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play { @@ -11,21 +21,140 @@ namespace osu.Game.Screens.Play /// public partial class DelayedResumeOverlay : ResumeOverlay { + private const double countdown_time = 800; + protected override LocalisableString Message => string.Empty; + [Resolved] + private OsuColour colours { get; set; } = null!; + private ScheduledDelegate? scheduledResume; + private int countdownCount = 3; + private double countdownStartTime; + + private Drawable content = null!; + private Drawable background = null!; + private SpriteText countdown = null!; + + public DelayedResumeOverlay() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load() + { + Add(content = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Masking = true, + BorderColour = colours.Yellow, + BorderThickness = 1, + Children = new[] + { + background = new Box + { + Size = new Vector2(250, 40), + Colour = Color4.Black, + Alpha = 0.8f + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Colour = colours.Yellow, + Children = new Drawable[] + { + // new Box + // { + // Anchor = Anchor.Centre, + // Origin = Anchor.Centre, + // Size = new Vector2(40, 3) + // }, + countdown = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + AlwaysPresent = true, + Font = OsuFont.Numeric.With(size: 20, fixedWidth: true) + }, + // new Box + // { + // Anchor = Anchor.Centre, + // Origin = Anchor.Centre, + // Size = new Vector2(40, 3) + // } + } + } + } + } + } + }); + } protected override void PopIn() { - base.PopIn(); + this.FadeIn(); + + content.FadeInFromZero(150, Easing.OutQuint); + content.ScaleTo(new Vector2(1.5f, 1)).Then().ScaleTo(1, 150, Easing.OutElasticQuarter); + + countdownCount = 3; + countdownStartTime = Time.Current; scheduledResume?.Cancel(); - scheduledResume = Scheduler.AddDelayed(Resume, 800); + scheduledResume = Scheduler.AddDelayed(Resume, countdown_time); + } + + protected override void Update() + { + base.Update(); + updateCountdown(); + } + + private void updateCountdown() + { + double amountTimePassed = Math.Min(countdown_time, Time.Current - countdownStartTime) / countdown_time; + int newCount = 3 - (int)Math.Floor(amountTimePassed * 3); + + if (newCount > 0) + { + countdown.Alpha = 1; + countdown.Text = newCount.ToString(); + } + else + countdown.Alpha = 0; + + if (newCount != countdownCount) + { + if (newCount == 0) + content.ScaleTo(new Vector2(1.5f, 1), 150, Easing.OutQuint); + else + content.ScaleTo(new Vector2(1.05f, 1), 50, Easing.OutQuint).Then().ScaleTo(1, 50, Easing.Out); + } + + countdownCount = newCount; } protected override void PopOut() { - base.PopOut(); + this.Delay(150).FadeOut(); + + content.FadeOut(150, Easing.OutQuint); + scheduledResume?.Cancel(); } } From cceb616a18cc862f975da533bed42b49a89d2fa9 Mon Sep 17 00:00:00 2001 From: Jayden Date: Mon, 4 Mar 2024 22:25:36 -0500 Subject: [PATCH 025/106] Update failure messages Co-authored-by: Salman Ahmed --- osu.Desktop/DiscordRichPresence.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 035add8044d9..85b612904360 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -184,14 +184,14 @@ private void onJoin(object sender, JoinMessage args) game.Window?.Raise(); if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) - Logger.Log("Failed to parse the room secret Discord gave us", LoggingTarget.Network, LogLevel.Error); + Logger.Log("Could not parse room from Discord RPC Client", LoggingTarget.Network, LogLevel.Important); var request = new GetRoomRequest(roomId); request.Success += room => Schedule(() => { game.PresentMultiplayerMatch(room, password); }); - request.Failure += _ => Logger.Log("Couldn't find the room Discord gave us", LoggingTarget.Network, LogLevel.Error); + request.Failure += _ => Logger.Log($"Could not find room {roomId} from Discord RPC Client", LoggingTarget.Network, LogLevel.Important); api.Queue(request); } From e6f1a722cbb54a905a20c91fe30a8be072ba357c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Mar 2024 18:48:58 +0100 Subject: [PATCH 026/106] Remove unused field and commented-out code --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 6f70a914f043..08d00f8ac273 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -33,7 +33,6 @@ public partial class DelayedResumeOverlay : ResumeOverlay private double countdownStartTime; private Drawable content = null!; - private Drawable background = null!; private SpriteText countdown = null!; public DelayedResumeOverlay() @@ -53,9 +52,9 @@ private void load() Masking = true, BorderColour = colours.Yellow, BorderThickness = 1, - Children = new[] + Children = new Drawable[] { - background = new Box + new Box { Size = new Vector2(250, 40), Colour = Color4.Black, @@ -75,29 +74,14 @@ private void load() AutoSizeAxes = Axes.Both, Spacing = new Vector2(5), Colour = colours.Yellow, - Children = new Drawable[] + Child = countdown = new OsuSpriteText { - // new Box - // { - // Anchor = Anchor.Centre, - // Origin = Anchor.Centre, - // Size = new Vector2(40, 3) - // }, - countdown = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - UseFullGlyphHeight = false, - AlwaysPresent = true, - Font = OsuFont.Numeric.With(size: 20, fixedWidth: true) - }, - // new Box - // { - // Anchor = Anchor.Centre, - // Origin = Anchor.Centre, - // Size = new Vector2(40, 3) - // } - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + AlwaysPresent = true, + Font = OsuFont.Numeric.With(size: 20, fixedWidth: true) + }, } } } From b53777c2a42bab857664cb5c2887e5cced0c0625 Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 5 Mar 2024 18:15:53 -0500 Subject: [PATCH 027/106] Refactor room secret handling to use JSON Also log room secrets for debugging purposes --- osu.Desktop/DiscordRichPresence.cs | 41 ++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 85b612904360..7315ee0c1719 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -5,6 +5,7 @@ using System.Text; using DiscordRPC; using DiscordRPC.Message; +using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -147,7 +148,13 @@ private void updateStatus() Size = room.Users.Count, }; - presence.Secrets.JoinSecret = $"{room.RoomID}:{room.Settings.Password}"; + RoomSecret roomSecret = new RoomSecret + { + RoomID = room.RoomID, + Password = room.Settings.Password, + }; + + presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); } else { @@ -182,9 +189,13 @@ private void updateStatus() private void onJoin(object sender, JoinMessage args) { game.Window?.Raise(); + Logger.Log($"Received room secret from Discord RPC Client: {args.Secret}", LoggingTarget.Network, LogLevel.Debug); if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) + { Logger.Log("Could not parse room from Discord RPC Client", LoggingTarget.Network, LogLevel.Important); + return; + } var request = new GetRoomRequest(roomId); request.Success += room => Schedule(() => @@ -216,21 +227,26 @@ private static string truncate(string str) }); } - private static bool tryParseRoomSecret(ReadOnlySpan secret, out long roomId, out string? password) + private static bool tryParseRoomSecret(string secretJson, out long roomId, out string? password) { roomId = 0; password = null; - int roomSecretSplitIndex = secret.IndexOf(':'); + RoomSecret? roomSecret; - if (roomSecretSplitIndex == -1) + try + { + roomSecret = JsonConvert.DeserializeObject(secretJson); + } + catch + { return false; + } - if (!long.TryParse(secret[..roomSecretSplitIndex], out roomId)) - return false; + if (roomSecret == null) return false; - password = secret[(roomSecretSplitIndex + 1)..].ToString(); - if (password.Length == 0) password = null; + roomId = roomSecret.RoomID; + password = roomSecret.Password; return true; } @@ -254,5 +270,14 @@ protected override void Dispose(bool isDisposing) client.Dispose(); base.Dispose(isDisposing); } + + private class RoomSecret + { + [JsonProperty(@"roomId", Required = Required.Always)] + public long RoomID { get; set; } + + [JsonProperty(@"password", Required = Required.AllowNull)] + public string? Password { get; set; } + } } } From 98713003176da6b09bafeea7392564959098956b Mon Sep 17 00:00:00 2001 From: Jayden Date: Tue, 5 Mar 2024 18:22:39 -0500 Subject: [PATCH 028/106] Improve language of user-facing errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 7315ee0c1719..8fecd015d404 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -193,7 +193,7 @@ private void onJoin(object sender, JoinMessage args) if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) { - Logger.Log("Could not parse room from Discord RPC Client", LoggingTarget.Network, LogLevel.Important); + Logger.Log("Could not join multiplayer room.", LoggingTarget.Network, LogLevel.Important); return; } From 98ca021e6628d94df508709482f047a2cff7cdde Mon Sep 17 00:00:00 2001 From: jvyden Date: Wed, 6 Mar 2024 01:17:11 -0500 Subject: [PATCH 029/106] Catch and warn about osu!stable lobbies --- osu.Desktop/DiscordRichPresence.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 8fecd015d404..b4a7e80d48f8 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -191,6 +191,15 @@ private void onJoin(object sender, JoinMessage args) game.Window?.Raise(); Logger.Log($"Received room secret from Discord RPC Client: {args.Secret}", LoggingTarget.Network, LogLevel.Debug); + // Stable and Lazer share the same Discord client ID, meaning they can accept join requests from each other. + // Since they aren't compatible in multi, see if stable's format is being used and log to avoid confusion. + // https://discord.com/channels/188630481301012481/188630652340404224/1214697229063946291 + if (args.Secret[0] != '{') + { + Logger.Log("osu!stable rooms are not compatible with lazer.", LoggingTarget.Network, LogLevel.Important); + return; + } + if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) { Logger.Log("Could not join multiplayer room.", LoggingTarget.Network, LogLevel.Important); From 1cafb0997713ee387f8994e972d64ad908aa4181 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 7 Mar 2024 17:22:38 +0900 Subject: [PATCH 030/106] Increase border thickness --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 08d00f8ac273..ba49810b2b35 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -51,7 +51,7 @@ private void load() AutoSizeAxes = Axes.Both, Masking = true, BorderColour = colours.Yellow, - BorderThickness = 1, + BorderThickness = 2, Children = new Drawable[] { new Box From 283de215d37aca9605bbf5b0a11dc440455bb348 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 11 Mar 2024 01:01:26 +0300 Subject: [PATCH 031/106] Adjust log message and comment --- osu.Desktop/DiscordRichPresence.cs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index b4a7e80d48f8..2e5db2f5c17e 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -189,20 +189,14 @@ private void updateStatus() private void onJoin(object sender, JoinMessage args) { game.Window?.Raise(); - Logger.Log($"Received room secret from Discord RPC Client: {args.Secret}", LoggingTarget.Network, LogLevel.Debug); - // Stable and Lazer share the same Discord client ID, meaning they can accept join requests from each other. - // Since they aren't compatible in multi, see if stable's format is being used and log to avoid confusion. - // https://discord.com/channels/188630481301012481/188630652340404224/1214697229063946291 - if (args.Secret[0] != '{') - { - Logger.Log("osu!stable rooms are not compatible with lazer.", LoggingTarget.Network, LogLevel.Important); - return; - } + Logger.Log($"Received room secret from Discord RPC Client: \"{args.Secret}\"", LoggingTarget.Network, LogLevel.Debug); - if (!tryParseRoomSecret(args.Secret, out long roomId, out string? password)) + // Stable and lazer share the same Discord client ID, meaning they can accept join requests from each other. + // Since they aren't compatible in multi, see if stable's format is being used and log to avoid confusion. + if (args.Secret[0] != '{' || !tryParseRoomSecret(args.Secret, out long roomId, out string? password)) { - Logger.Log("Could not join multiplayer room.", LoggingTarget.Network, LogLevel.Important); + Logger.Log("Could not join multiplayer room, invitation is invalid or incompatible.", LoggingTarget.Network, LogLevel.Important); return; } @@ -211,7 +205,7 @@ private void onJoin(object sender, JoinMessage args) { game.PresentMultiplayerMatch(room, password); }); - request.Failure += _ => Logger.Log($"Could not find room {roomId} from Discord RPC Client", LoggingTarget.Network, LogLevel.Important); + request.Failure += _ => Logger.Log($"Could not join multiplayer room, room could not be found (room ID: {roomId}).", LoggingTarget.Network, LogLevel.Important); api.Queue(request); } From 226df7163e1b34eb4a78f02f0038493b673a8dce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Mar 2024 16:55:49 +0800 Subject: [PATCH 032/106] Update client ID --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 2e5db2f5c17e..4e3db2db2d86 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -25,7 +25,7 @@ namespace osu.Desktop { internal partial class DiscordRichPresence : Component { - private const string client_id = "367827983903490050"; + private const string client_id = "1216669957799018608"; private DiscordRpcClient client = null!; From 169e2e1b4e21c824e586cd1be88b33875e9a2e30 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 11 Mar 2024 09:54:49 +0300 Subject: [PATCH 033/106] Change maximum room number to closest powers of two --- osu.Desktop/DiscordRichPresence.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 4e3db2db2d86..886038bcf0e7 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -142,9 +142,8 @@ private void updateStatus() Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private, ID = room.RoomID.ToString(), // technically lobbies can have infinite users, but Discord needs this to be set to something. - // 1024 just happens to look nice. - // https://discord.com/channels/188630481301012481/188630652340404224/1212967974793642034 - Max = 1024, + // to make party display sensible, assign a powers of two above participants count (8 at minimum). + Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))), Size = room.Users.Count, }; From 8b730acb082379174f9a48b5fd132b184b4e9a81 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 11 Mar 2024 11:18:59 +0300 Subject: [PATCH 034/106] Update presence on changes to multiplayer room --- osu.Desktop/DiscordRichPresence.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 886038bcf0e7..8de1a08e7a32 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game; @@ -91,6 +92,8 @@ private void load(OsuConfigManager config) activity.BindValueChanged(_ => updateStatus()); privacyMode.BindValueChanged(_ => updateStatus()); + multiplayerClient.RoomUpdated += updateStatus; + client.Initialize(); } @@ -269,6 +272,9 @@ private static bool tryParseRoomSecret(string secretJson, out long roomId, out s protected override void Dispose(bool isDisposing) { + if (multiplayerClient.IsNotNull()) + multiplayerClient.RoomUpdated -= updateStatus; + client.Dispose(); base.Dispose(isDisposing); } From 5580ce31fa6c37c7c65a703cfe820705928453bd Mon Sep 17 00:00:00 2001 From: jvyden Date: Mon, 11 Mar 2024 18:15:18 -0400 Subject: [PATCH 035/106] Log Discord RPC updates --- osu.Desktop/DiscordRichPresence.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 8de1a08e7a32..6c8bd26d3dc7 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -114,6 +114,8 @@ private void updateStatus() return; } + Logger.Log("Updating Discord RPC", LoggingTarget.Network, LogLevel.Debug); + if (activity.Value != null) { bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; From e4e7dd14f30ee5c2d28ccc967a0a891fbbd076b7 Mon Sep 17 00:00:00 2001 From: jvyden Date: Mon, 11 Mar 2024 18:16:13 -0400 Subject: [PATCH 036/106] Revert "Update presence on changes to multiplayer room" This reverts commit 8b730acb082379174f9a48b5fd132b184b4e9a81. --- osu.Desktop/DiscordRichPresence.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 6c8bd26d3dc7..ca26cab0fddd 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -8,7 +8,6 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game; @@ -92,8 +91,6 @@ private void load(OsuConfigManager config) activity.BindValueChanged(_ => updateStatus()); privacyMode.BindValueChanged(_ => updateStatus()); - multiplayerClient.RoomUpdated += updateStatus; - client.Initialize(); } @@ -274,9 +271,6 @@ private static bool tryParseRoomSecret(string secretJson, out long roomId, out s protected override void Dispose(bool isDisposing) { - if (multiplayerClient.IsNotNull()) - multiplayerClient.RoomUpdated -= updateStatus; - client.Dispose(); base.Dispose(isDisposing); } From 789a9f4dfa071dacc9157914e93792231d568923 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 14 Mar 2024 11:01:57 +0900 Subject: [PATCH 037/106] Initial redesign following flyte's design --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 125 +++++++++++------- 1 file changed, 79 insertions(+), 46 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index ba49810b2b35..e9dd26a06b08 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -3,10 +3,12 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Game.Graphics; @@ -21,7 +23,12 @@ namespace osu.Game.Screens.Play /// public partial class DelayedResumeOverlay : ResumeOverlay { - private const double countdown_time = 800; + private const float outer_size = 200; + private const float inner_size = 150; + private const float progress_stroke_width = 7; + private const float progress_size = inner_size + progress_stroke_width / 2f; + + private const double countdown_time = 3000; protected override LocalisableString Message => string.Empty; @@ -31,9 +38,15 @@ public partial class DelayedResumeOverlay : ResumeOverlay private ScheduledDelegate? scheduledResume; private int countdownCount = 3; private double countdownStartTime; + private bool countdownComplete; + + private Drawable outerContent = null!; + private Container innerContent = null!; - private Drawable content = null!; - private SpriteText countdown = null!; + private Container countdownComponents = null!; + private Drawable countdownBackground = null!; + private SpriteText countdownText = null!; + private CircularProgress countdownProgress = null!; public DelayedResumeOverlay() { @@ -44,44 +57,48 @@ public DelayedResumeOverlay() [BackgroundDependencyLoader] private void load() { - Add(content = new CircularContainer + Add(outerContent = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(outer_size), + Colour = Color4.Black.Opacity(0.25f) + }); + + Add(innerContent = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Masking = true, - BorderColour = colours.Yellow, - BorderThickness = 2, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Children = new[] { - new Box - { - Size = new Vector2(250, 40), - Colour = Color4.Black, - Alpha = 0.8f - }, - new Container + countdownBackground = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, + Size = new Vector2(inner_size), + Colour = Color4.Black.Opacity(0.25f) + }, + countdownComponents = new Container + { + RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new FillFlowContainer + countdownProgress = new CircularProgress { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Colour = colours.Yellow, - Child = countdown = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - UseFullGlyphHeight = false, - AlwaysPresent = true, - Font = OsuFont.Numeric.With(size: 20, fixedWidth: true) - }, + Size = new Vector2(progress_size), + InnerRadius = progress_stroke_width / progress_size, + RoundedCaps = true + }, + countdownText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + AlwaysPresent = true, + Font = OsuFont.Torus.With(size: 70, weight: FontWeight.Light) } } } @@ -93,14 +110,26 @@ protected override void PopIn() { this.FadeIn(); - content.FadeInFromZero(150, Easing.OutQuint); - content.ScaleTo(new Vector2(1.5f, 1)).Then().ScaleTo(1, 150, Easing.OutElasticQuarter); + // The transition effects. + outerContent.FadeIn().ScaleTo(Vector2.Zero).Then().ScaleTo(Vector2.One, 200, Easing.OutQuint); + innerContent.FadeIn().ScaleTo(Vector2.Zero).Then().ScaleTo(Vector2.One, 400, Easing.OutElasticHalf); + countdownComponents.FadeOut().Then().Delay(50).FadeTo(1, 100); + + // Reset states for various components. + countdownBackground.FadeIn(); + countdownText.FadeIn(); + countdownProgress.FadeIn().ScaleTo(1); + countdownComplete = false; countdownCount = 3; countdownStartTime = Time.Current; scheduledResume?.Cancel(); - scheduledResume = Scheduler.AddDelayed(Resume, countdown_time); + scheduledResume = Scheduler.AddDelayed(() => + { + countdownComplete = true; + Resume(); + }, countdown_time); } protected override void Update() @@ -114,20 +143,14 @@ private void updateCountdown() double amountTimePassed = Math.Min(countdown_time, Time.Current - countdownStartTime) / countdown_time; int newCount = 3 - (int)Math.Floor(amountTimePassed * 3); - if (newCount > 0) - { - countdown.Alpha = 1; - countdown.Text = newCount.ToString(); - } - else - countdown.Alpha = 0; + countdownProgress.Current.Value = amountTimePassed; + countdownText.Text = Math.Max(1, newCount).ToString(); + countdownProgress.InnerRadius = progress_stroke_width / progress_size / countdownProgress.Scale.X; - if (newCount != countdownCount) + if (countdownCount != newCount && newCount > 0) { - if (newCount == 0) - content.ScaleTo(new Vector2(1.5f, 1), 150, Easing.OutQuint); - else - content.ScaleTo(new Vector2(1.05f, 1), 50, Easing.OutQuint).Then().ScaleTo(1, 50, Easing.Out); + countdownText.ScaleTo(0.25f).Then().ScaleTo(1, 200, Easing.OutQuint); + outerContent.Delay(25).Then().ScaleTo(1.05f, 100).Then().ScaleTo(1f, 200, Easing.Out); } countdownCount = newCount; @@ -135,9 +158,19 @@ private void updateCountdown() protected override void PopOut() { - this.Delay(150).FadeOut(); + this.Delay(300).FadeOut(); + + outerContent.FadeOut(); + countdownBackground.FadeOut(); + countdownText.FadeOut(); - content.FadeOut(150, Easing.OutQuint); + if (countdownComplete) + { + countdownProgress.ScaleTo(2f, 300, Easing.OutQuint); + countdownProgress.Delay(200).FadeOut(100, Easing.Out); + } + else + countdownProgress.FadeOut(); scheduledResume?.Cancel(); } From b431bb11764c4c4088eea49f4cd7f5e7611e4aa9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 14 Mar 2024 12:24:12 +0900 Subject: [PATCH 038/106] Resolve post-merge issues --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index e9dd26a06b08..196ca24358a3 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -32,9 +32,6 @@ public partial class DelayedResumeOverlay : ResumeOverlay protected override LocalisableString Message => string.Empty; - [Resolved] - private OsuColour colours { get; set; } = null!; - private ScheduledDelegate? scheduledResume; private int countdownCount = 3; private double countdownStartTime; @@ -143,7 +140,7 @@ private void updateCountdown() double amountTimePassed = Math.Min(countdown_time, Time.Current - countdownStartTime) / countdown_time; int newCount = 3 - (int)Math.Floor(amountTimePassed * 3); - countdownProgress.Current.Value = amountTimePassed; + countdownProgress.Progress = amountTimePassed; countdownText.Text = Math.Max(1, newCount).ToString(); countdownProgress.InnerRadius = progress_stroke_width / progress_size / countdownProgress.Scale.X; From b309aad895b001e042ec85f2c6103457fdd801a1 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 8 Mar 2024 13:13:08 -0800 Subject: [PATCH 039/106] Fix noticeable masking artifact of beatmap cards when already downloaded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Had a similar fix before seeing https://github.com/ppy/osu/pull/20743#discussion_r994955470, but using that diff instead so co-authoring. Co-Authored-By: Bartłomiej Dach --- .../Drawables/Cards/CollapsibleButtonContainer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs index fe2ee8c7cc6a..32df1755a701 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -77,7 +77,7 @@ public CollapsibleButtonContainer(APIBeatmapSet beatmapSet) downloadTracker, background = new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Y, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Child = new Box @@ -165,9 +165,11 @@ protected override void LoadComplete() private void updateState() { - float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth); + float buttonAreaWidth = ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth; + float mainAreaWidth = Width - buttonAreaWidth; - mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + mainArea.ResizeWidthTo(mainAreaWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + background.ResizeWidthTo(buttonAreaWidth + BeatmapCard.CORNER_RADIUS, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); buttons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); From b4cee12db96644a8364ed1525aea51f526c1dcb6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 14 Mar 2024 09:21:29 +0300 Subject: [PATCH 040/106] Use defined colours for counter background --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 196ca24358a3..c6cfeca142c4 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -13,8 +12,8 @@ using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Play { @@ -54,12 +53,15 @@ public DelayedResumeOverlay() [BackgroundDependencyLoader] private void load() { + // todo: this shouldn't define its own colour provider, but nothing in Player screen does, so let's do that for now. + var colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + Add(outerContent = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(outer_size), - Colour = Color4.Black.Opacity(0.25f) + Colour = colourProvider.Background6, }); Add(innerContent = new Container @@ -74,7 +76,7 @@ private void load() Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(inner_size), - Colour = Color4.Black.Opacity(0.25f) + Colour = colourProvider.Background4, }, countdownComponents = new Container { From f8a841e907e9b2deb3ca08c425a0e665a5c07775 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 14 Mar 2024 00:17:22 -0700 Subject: [PATCH 041/106] Add comment explaining why width is limited to the button area Same comment as https://github.com/ppy/osu/blob/a47ccb8edd2392258b6b7e176b222a9ecd511fc0/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs#L91-L92. --- osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs index 32df1755a701..a29724032e6b 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -169,6 +169,8 @@ private void updateState() float mainAreaWidth = Width - buttonAreaWidth; mainArea.ResizeWidthTo(mainAreaWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. background.ResizeWidthTo(buttonAreaWidth + BeatmapCard.CORNER_RADIUS, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); From 42bd558d7c159857ca7356b8485d710802685fc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Mar 2024 22:41:29 +0800 Subject: [PATCH 042/106] Only update text when necessary (reducing unnecessary string allocadtions) --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index c6cfeca142c4..468e67901d0b 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -32,7 +32,7 @@ public partial class DelayedResumeOverlay : ResumeOverlay protected override LocalisableString Message => string.Empty; private ScheduledDelegate? scheduledResume; - private int countdownCount = 3; + private int? countdownCount; private double countdownStartTime; private bool countdownComplete; @@ -120,7 +120,7 @@ protected override void PopIn() countdownProgress.FadeIn().ScaleTo(1); countdownComplete = false; - countdownCount = 3; + countdownCount = null; countdownStartTime = Time.Current; scheduledResume?.Cancel(); @@ -143,11 +143,11 @@ private void updateCountdown() int newCount = 3 - (int)Math.Floor(amountTimePassed * 3); countdownProgress.Progress = amountTimePassed; - countdownText.Text = Math.Max(1, newCount).ToString(); countdownProgress.InnerRadius = progress_stroke_width / progress_size / countdownProgress.Scale.X; if (countdownCount != newCount && newCount > 0) { + countdownText.Text = Math.Max(1, newCount).ToString(); countdownText.ScaleTo(0.25f).Then().ScaleTo(1, 200, Easing.OutQuint); outerContent.Delay(25).Then().ScaleTo(1.05f, 100).Then().ScaleTo(1f, 200, Easing.Out); } From d7769ec3e25cc985fea44e6fb0d02ab668e2dd33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Mar 2024 22:38:59 +0800 Subject: [PATCH 043/106] Adjust animation --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 468e67901d0b..7c34050bbfb3 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -112,7 +112,7 @@ protected override void PopIn() // The transition effects. outerContent.FadeIn().ScaleTo(Vector2.Zero).Then().ScaleTo(Vector2.One, 200, Easing.OutQuint); innerContent.FadeIn().ScaleTo(Vector2.Zero).Then().ScaleTo(Vector2.One, 400, Easing.OutElasticHalf); - countdownComponents.FadeOut().Then().Delay(50).FadeTo(1, 100); + countdownComponents.FadeOut().Delay(50).FadeTo(1, 100); // Reset states for various components. countdownBackground.FadeIn(); @@ -166,7 +166,7 @@ protected override void PopOut() if (countdownComplete) { countdownProgress.ScaleTo(2f, 300, Easing.OutQuint); - countdownProgress.Delay(200).FadeOut(100, Easing.Out); + countdownProgress.FadeOut(100, Easing.Out); } else countdownProgress.FadeOut(); From 888245b44fa13e46a7fb23b4a801f3fd36d38fee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Mar 2024 22:44:26 +0800 Subject: [PATCH 044/106] Reorder methods --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 7c34050bbfb3..4b703ec3cf58 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -131,6 +131,25 @@ protected override void PopIn() }, countdown_time); } + protected override void PopOut() + { + this.Delay(300).FadeOut(); + + outerContent.FadeOut(); + countdownBackground.FadeOut(); + countdownText.FadeOut(); + + if (countdownComplete) + { + countdownProgress.ScaleTo(2f, 300, Easing.OutQuint); + countdownProgress.FadeOut(100, Easing.Out); + } + else + countdownProgress.FadeOut(); + + scheduledResume?.Cancel(); + } + protected override void Update() { base.Update(); @@ -154,24 +173,5 @@ private void updateCountdown() countdownCount = newCount; } - - protected override void PopOut() - { - this.Delay(300).FadeOut(); - - outerContent.FadeOut(); - countdownBackground.FadeOut(); - countdownText.FadeOut(); - - if (countdownComplete) - { - countdownProgress.ScaleTo(2f, 300, Easing.OutQuint); - countdownProgress.FadeOut(100, Easing.Out); - } - else - countdownProgress.FadeOut(); - - scheduledResume?.Cancel(); - } } } From 2845303a74986d4310d7fb4a77b56a1bd980a8d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Mar 2024 22:44:57 +0800 Subject: [PATCH 045/106] Fix non-matching scale outwards animation --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 4b703ec3cf58..c3c98510e37e 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -142,7 +142,7 @@ protected override void PopOut() if (countdownComplete) { countdownProgress.ScaleTo(2f, 300, Easing.OutQuint); - countdownProgress.FadeOut(100, Easing.Out); + countdownProgress.FadeOut(300, Easing.OutQuint); } else countdownProgress.FadeOut(); From 23975d4dd162b503ccb0e79f0c0d570c5606f901 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Mar 2024 22:49:53 +0800 Subject: [PATCH 046/106] Add flash and reduce overall time for countdown to 2 seconds --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index c3c98510e37e..fd1ce5d82922 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -22,12 +22,15 @@ namespace osu.Game.Screens.Play /// public partial class DelayedResumeOverlay : ResumeOverlay { + // todo: this shouldn't define its own colour provider, but nothing in Player screen does, so let's do that for now. + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + private const float outer_size = 200; private const float inner_size = 150; private const float progress_stroke_width = 7; private const float progress_size = inner_size + progress_stroke_width / 2f; - private const double countdown_time = 3000; + private const double countdown_time = 2000; protected override LocalisableString Message => string.Empty; @@ -53,9 +56,6 @@ public DelayedResumeOverlay() [BackgroundDependencyLoader] private void load() { - // todo: this shouldn't define its own colour provider, but nothing in Player screen does, so let's do that for now. - var colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - Add(outerContent = new Circle { Anchor = Anchor.Centre, @@ -169,6 +169,8 @@ private void updateCountdown() countdownText.Text = Math.Max(1, newCount).ToString(); countdownText.ScaleTo(0.25f).Then().ScaleTo(1, 200, Easing.OutQuint); outerContent.Delay(25).Then().ScaleTo(1.05f, 100).Then().ScaleTo(1f, 200, Easing.Out); + + countdownBackground.FlashColour(colourProvider.Background3, 400, Easing.Out); } countdownCount = newCount; From ea3a9314f9dd3bb19fcc0aff6c1b212cf96cc560 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 13 Mar 2024 23:08:00 +0300 Subject: [PATCH 047/106] Improve TimelineControlPointDisplay performance --- .../Timeline/TimelineControlPointDisplay.cs | 90 ++++++++++++++----- .../Components/Timeline/TopPointPiece.cs | 6 +- 2 files changed, 71 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index cd97b293ba8f..60d113ef5852 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Specialized; -using System.Diagnostics; -using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -15,6 +14,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public partial class TimelineControlPointDisplay : TimelinePart { + [Resolved] + private Timeline? timeline { get; set; } + + /// + /// The visible time/position range of the timeline. + /// + private (float min, float max) visibleRange = (float.MinValue, float.MaxValue); + + private readonly Cached groupCache = new Cached(); + private readonly IBindableList controlPointGroups = new BindableList(); protected override void LoadBeatmap(EditorBeatmap beatmap) @@ -23,34 +32,71 @@ protected override void LoadBeatmap(EditorBeatmap beatmap) controlPointGroups.UnbindAll(); controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((_, args) => + controlPointGroups.BindCollectionChanged((_, _) => { - switch (args.Action) - { - case NotifyCollectionChangedAction.Reset: - Clear(); - break; + invalidateGroups(); + }, true); + } - case NotifyCollectionChangedAction.Add: - Debug.Assert(args.NewItems != null); + protected override void Update() + { + base.Update(); - foreach (var group in args.NewItems.OfType()) - Add(new TimelineControlPointGroup(group)); - break; + if (timeline == null || DrawWidth <= 0) return; + + (float, float) newRange = ( + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - TopPointPiece.WIDTH) / DrawWidth * Content.RelativeChildSize.X, + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X) / DrawWidth * Content.RelativeChildSize.X); - case NotifyCollectionChangedAction.Remove: - Debug.Assert(args.OldItems != null); + if (visibleRange != newRange) + { + visibleRange = newRange; + invalidateGroups(); + } - foreach (var group in args.OldItems.OfType()) - { - var matching = Children.SingleOrDefault(gv => ReferenceEquals(gv.Group, group)); + if (!groupCache.IsValid) + recreateDrawableGroups(); + } - matching?.Expire(); - } + private void invalidateGroups() => groupCache.Invalidate(); + private void recreateDrawableGroups() + { + // Remove groups outside the visible range + for (int i = Count - 1; i >= 0; i--) + { + var g = Children[i]; + + if (!shouldBeVisible(g.Group)) + g.Expire(); + } + + // Add remaining ones + foreach (var group in controlPointGroups) + { + if (!shouldBeVisible(group)) + continue; + + bool alreadyVisible = false; + + foreach (var g in this) + { + if (ReferenceEquals(g.Group, group)) + { + alreadyVisible = true; break; + } } - }, true); + + if (alreadyVisible) + continue; + + Add(new TimelineControlPointGroup(group)); + } + + groupCache.Validate(); } + + private bool shouldBeVisible(ControlPointGroup group) => group.Time >= visibleRange.min && group.Time <= visibleRange.max; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs index 243cdc6ddd4c..a40a80536158 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs @@ -19,12 +19,12 @@ public partial class TopPointPiece : CompositeDrawable protected OsuSpriteText Label { get; private set; } = null!; - private const float width = 80; + public const float WIDTH = 80; public TopPointPiece(ControlPoint point) { Point = point; - Width = width; + Width = WIDTH; Height = 16; Margin = new MarginPadding { Vertical = 4 }; @@ -65,7 +65,7 @@ private void load(OsuColour colours) new Container { RelativeSizeAxes = Axes.Y, - Width = width - triangle_portion, + Width = WIDTH - triangle_portion, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Colour = Point.GetRepresentingColour(colours), From e825db61eeebc0e1f94b2f62b6dba30c478dd58a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 16 Mar 2024 12:26:56 +0300 Subject: [PATCH 048/106] Fix enumerator allocation --- .../Components/Timeline/TimelineControlPointDisplay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 60d113ef5852..950b717ffb75 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -72,8 +72,10 @@ private void recreateDrawableGroups() } // Add remaining ones - foreach (var group in controlPointGroups) + for (int i = 0; i < controlPointGroups.Count; i++) { + var group = controlPointGroups[i]; + if (!shouldBeVisible(group)) continue; From 981ee54cdca16653753c85a71192f404cb80e85c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 16 Mar 2024 15:05:52 +0300 Subject: [PATCH 049/106] Fix transforms overhead in TimelineTickDisplay --- .../Edit/Compose/Components/Timeline/TimelineTickDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 7e7bef8cf29f..5348d03a385a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -165,7 +165,7 @@ private void createTicks() // save a few drawables beyond the currently used for edge cases. while (drawableIndex < Math.Min(usedDrawables + 16, Count)) - Children[drawableIndex++].Hide(); + Children[drawableIndex++].Alpha = 0; // expire any excess while (drawableIndex < Count) @@ -182,7 +182,7 @@ Drawable getNextUsableLine() point = Children[drawableIndex]; drawableIndex++; - point.Show(); + point.Alpha = 1; return point; } From 34a5e2d606070ba043afc501030933bd4f158412 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 16 Mar 2024 15:20:37 +0300 Subject: [PATCH 050/106] Don't update subtree masking in TimelineTickDisplay --- .../Edit/Compose/Components/Timeline/TimelineTickDisplay.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 5348d03a385a..c3adb43032cb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -18,6 +19,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public partial class TimelineTickDisplay : TimelinePart { + // With current implementation every tick in the sub-tree should be visible, no need to check whether they are masked away. + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; + [Resolved] private EditorBeatmap beatmap { get; set; } = null!; From 63816adbc081065fe68b9bc20b49b9329c4abbd2 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 16 Mar 2024 21:20:12 -0300 Subject: [PATCH 051/106] Add verify checks to unused audio at the end --- .../Checks/CheckUnusedAudioAtEndTest.cs | 90 +++++++++++++++++++ .../CheckUnusedAudioAtEndStrings.cs | 19 ++++ osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 2 + .../Edit/Checks/CheckUnusedAudioAtEnd.cs | 50 +++++++++++ 4 files changed, 161 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs create mode 100644 osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs new file mode 100644 index 000000000000..687feae63db3 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs @@ -0,0 +1,90 @@ +// 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 Moq; +using NUnit.Framework; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; + +namespace osu.Game.Tests.Editing.Checks +{ + public class CheckUnusedAudioTest + { + private CheckUnusedAudioAtEnd check = null!; + + private IBeatmap beatmapNotFullyMapped = null!; + + private IBeatmap beatmapFullyMapped = null!; + + [SetUp] + public void Setup() + { + check = new CheckUnusedAudioAtEnd(); + beatmapNotFullyMapped = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 1_298 }, + }, + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" } + } + }; + beatmapFullyMapped = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 9000 }, + }, + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" } + } + }; + } + + [Test] + public void TestAudioNotFullyUsed() + { + var context = getContext(beatmapNotFullyMapped); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEnd); + } + + [Test] + public void TestAudioFullyUsed() + { + var context = getContext(beatmapFullyMapped); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + private BeatmapVerifierContext getContext(IBeatmap beatmap) + { + return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(beatmap).Object); + } + + private Mock getMockWorkingBeatmap(IBeatmap beatmap) + { + var mockTrack = new TrackVirtualStore(new FramedClock()).GetVirtual(10000, "virtual"); + + var mockWorkingBeatmap = new Mock(); + mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap); + mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack); + + return mockWorkingBeatmap; + } + } +} diff --git a/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs b/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs new file mode 100644 index 000000000000..46f92237a967 --- /dev/null +++ b/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs @@ -0,0 +1,19 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class CheckUnusedAudioAtEndStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.CheckUnusedAudioAtEnd"; + + /// + /// "{0}% of the audio is not mapped." + /// + public static LocalisableString OfTheAudioIsNot(double percentageLeft) => new TranslatableString(getKey(@"of_the_audio_is_not"), @"{0}% of the audio is not mapped.", percentageLeft); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index dcf5eb4da9cd..4bba72d82893 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -36,12 +36,14 @@ public class BeatmapVerifier : IBeatmapVerifier new CheckConcurrentObjects(), new CheckZeroLengthObjects(), new CheckDrainLength(), + new CheckUnusedAudioAtEnd(), // Timing new CheckPreviewTime(), // Events new CheckBreaks() + }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs new file mode 100644 index 000000000000..c120e0993af4 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.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; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckUnusedAudioAtEnd : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Compose, "More than 20% unused audio at the end"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateUnusedAudioAtEnd(this), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + double mappedLength = context.Beatmap.HitObjects.Last().GetEndTime(); + double trackLength = context.WorkingBeatmap.Track.Length; + + double mappedPercentage = calculatePercentage(mappedLength, trackLength); + + if (mappedPercentage < 80) + { + yield return new IssueTemplateUnusedAudioAtEnd(this).Create(); + } + + } + + private double calculatePercentage(double mappedLenght, double trackLenght) + { + return Math.Round(mappedLenght / trackLenght * 100); + } + + public class IssueTemplateUnusedAudioAtEnd : IssueTemplate + { + public IssueTemplateUnusedAudioAtEnd(ICheck check) + : base(check, IssueType.Problem, "There is more than 20% unused audio at the end.") + { + } + + public Issue Create() => new Issue(this); + } + } +} From f7aff76592c108ff3df1b97b3d2b8e8d7ded6938 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 16 Mar 2024 23:03:06 -0300 Subject: [PATCH 052/106] Fix codefactor issues --- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 1 - osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 4bba72d82893..4a316afd2226 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -43,7 +43,6 @@ public class BeatmapVerifier : IBeatmapVerifier // Events new CheckBreaks() - }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index c120e0993af4..9c1f2748e911 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -29,7 +29,6 @@ public IEnumerable Run(BeatmapVerifierContext context) { yield return new IssueTemplateUnusedAudioAtEnd(this).Create(); } - } private double calculatePercentage(double mappedLenght, double trackLenght) From 80f24a07916e9ae03fa4631e7f9018812139214d Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 16 Mar 2024 23:30:59 -0300 Subject: [PATCH 053/106] Fix test class name not matching file name --- osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs index 687feae63db3..29c5cb96fdd5 100644 --- a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Editing.Checks { - public class CheckUnusedAudioTest + public class CheckUnusedAudioAtEndTest { private CheckUnusedAudioAtEnd check = null!; From a3f3dcf853b6148c6c2cbebe41b88711c01d468a Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 18 Mar 2024 13:27:43 -0300 Subject: [PATCH 054/106] Inline percentage calculation --- osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index 9c1f2748e911..8795eeac2da2 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -23,7 +23,7 @@ public IEnumerable Run(BeatmapVerifierContext context) double mappedLength = context.Beatmap.HitObjects.Last().GetEndTime(); double trackLength = context.WorkingBeatmap.Track.Length; - double mappedPercentage = calculatePercentage(mappedLength, trackLength); + double mappedPercentage = Math.Round(mappedLength / trackLength * 100); if (mappedPercentage < 80) { @@ -31,11 +31,6 @@ public IEnumerable Run(BeatmapVerifierContext context) } } - private double calculatePercentage(double mappedLenght, double trackLenght) - { - return Math.Round(mappedLenght / trackLenght * 100); - } - public class IssueTemplateUnusedAudioAtEnd : IssueTemplate { public IssueTemplateUnusedAudioAtEnd(ICheck check) From 915a9682b5be54b090d39ae2a44beae8ea2c9ce7 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 18 Mar 2024 13:51:36 -0300 Subject: [PATCH 055/106] Fix issue type and display percentage left --- osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index 8795eeac2da2..d22303b7df5a 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -27,18 +27,19 @@ public IEnumerable Run(BeatmapVerifierContext context) if (mappedPercentage < 80) { - yield return new IssueTemplateUnusedAudioAtEnd(this).Create(); + double percentageLeft = Math.Abs(mappedPercentage - 100); + yield return new IssueTemplateUnusedAudioAtEnd(this).Create(percentageLeft); } } public class IssueTemplateUnusedAudioAtEnd : IssueTemplate { public IssueTemplateUnusedAudioAtEnd(ICheck check) - : base(check, IssueType.Problem, "There is more than 20% unused audio at the end.") + : base(check, IssueType.Warning, "Currently there is {0}% unused audio at the end. Ensure the outro significantly contributes to the song, otherwise cut the outro.") { } - public Issue Create() => new Issue(this); + public Issue Create(double percentageLeft) => new Issue(this, percentageLeft); } } } From c23212f4efad0c58207265877a5130177f47d2e5 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 18 Mar 2024 14:02:33 -0300 Subject: [PATCH 056/106] Use `GetLastObjectTime` to calculate mapped length --- osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index d22303b7df5a..d9a675fd1713 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -3,9 +3,8 @@ using System; using System.Collections.Generic; -using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks.Components; -using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Edit.Checks { @@ -20,7 +19,7 @@ public class CheckUnusedAudioAtEnd : ICheck public IEnumerable Run(BeatmapVerifierContext context) { - double mappedLength = context.Beatmap.HitObjects.Last().GetEndTime(); + double mappedLength = context.Beatmap.GetLastObjectTime(); double trackLength = context.WorkingBeatmap.Track.Length; double mappedPercentage = Math.Round(mappedLength / trackLength * 100); From 0edc249637d56208bca655db17e274738df0b70e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 18 Mar 2024 20:38:19 +0300 Subject: [PATCH 057/106] Make Timeline non-nullable --- .../Components/Timeline/TimelineControlPointDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 950b717ffb75..1bf12e40d199 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public partial class TimelineControlPointDisplay : TimelinePart { [Resolved] - private Timeline? timeline { get; set; } + private Timeline timeline { get; set; } = null!; /// /// The visible time/position range of the timeline. @@ -42,7 +42,7 @@ protected override void Update() { base.Update(); - if (timeline == null || DrawWidth <= 0) return; + if (DrawWidth <= 0) return; (float, float) newRange = ( (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - TopPointPiece.WIDTH) / DrawWidth * Content.RelativeChildSize.X, From 7ca45c75b3348f66c0ec86e02a72c114abb27c8c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 18 Mar 2024 20:46:38 +0300 Subject: [PATCH 058/106] Don't iterate backwards on children without a reason --- .../Components/Timeline/TimelineControlPointDisplay.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 1bf12e40d199..8e522fa715f6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -63,12 +63,10 @@ protected override void Update() private void recreateDrawableGroups() { // Remove groups outside the visible range - for (int i = Count - 1; i >= 0; i--) + foreach (var drawableGroup in this) { - var g = Children[i]; - - if (!shouldBeVisible(g.Group)) - g.Expire(); + if (!shouldBeVisible(drawableGroup.Group)) + drawableGroup.Expire(); } // Add remaining ones From f6d7f18f2592071d98872e2cda18f8de9d20cfa1 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 18 Mar 2024 15:59:19 -0300 Subject: [PATCH 059/106] Remove unused localisation file --- .../CheckUnusedAudioAtEndStrings.cs | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs diff --git a/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs b/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs deleted file mode 100644 index 46f92237a967..000000000000 --- a/osu.Game/Localisation/CheckUnusedAudioAtEndStrings.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.Localisation; - -namespace osu.Game.Localisation -{ - public static class CheckUnusedAudioAtEndStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.CheckUnusedAudioAtEnd"; - - /// - /// "{0}% of the audio is not mapped." - /// - public static LocalisableString OfTheAudioIsNot(double percentageLeft) => new TranslatableString(getKey(@"of_the_audio_is_not"), @"{0}% of the audio is not mapped.", percentageLeft); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} From 5241c999c1f3300f36723a1d070c23240936c8da Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 18 Mar 2024 16:08:41 -0300 Subject: [PATCH 060/106] Add different warning to maps with storyboard/video --- .../Checks/CheckUnusedAudioAtEndTest.cs | 51 +++++++++++++++++-- .../Edit/Checks/CheckUnusedAudioAtEnd.cs | 37 +++++++++++++- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs index 29c5cb96fdd5..33d73a808644 100644 --- a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs @@ -4,12 +4,15 @@ using System.Linq; using Moq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Storyboards; +using osuTK; using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; namespace osu.Game.Tests.Editing.Checks @@ -47,7 +50,7 @@ public void Setup() }, BeatmapInfo = new BeatmapInfo { - Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" } + Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" }, } }; } @@ -62,6 +65,42 @@ public void TestAudioNotFullyUsed() Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEnd); } + [Test] + public void TestAudioNotFullyUsedWithVideo() + { + var storyboard = new Storyboard(); + + var video = new StoryboardVideo("abc123.mp4", 0); + + storyboard.GetLayer("Video").Add(video); + + var mockWorkingBeatmap = getMockWorkingBeatmap(beatmapNotFullyMapped, storyboard); + + var context = getContext(beatmapNotFullyMapped, mockWorkingBeatmap); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEndStoryboardOrVideo); + } + + [Test] + public void TestAudioNotFullyUsedWithStoryboardElement() + { + var storyboard = new Storyboard(); + + var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); + + storyboard.GetLayer("Background").Add(sprite); + + var mockWorkingBeatmap = getMockWorkingBeatmap(beatmapNotFullyMapped, storyboard); + + var context = getContext(beatmapNotFullyMapped, mockWorkingBeatmap); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEndStoryboardOrVideo); + } + [Test] public void TestAudioFullyUsed() { @@ -73,16 +112,22 @@ public void TestAudioFullyUsed() private BeatmapVerifierContext getContext(IBeatmap beatmap) { - return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(beatmap).Object); + return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(beatmap, new Storyboard()).Object); + } + + private BeatmapVerifierContext getContext(IBeatmap beatmap, Mock workingBeatmap) + { + return new BeatmapVerifierContext(beatmap, workingBeatmap.Object); } - private Mock getMockWorkingBeatmap(IBeatmap beatmap) + private Mock getMockWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard) { var mockTrack = new TrackVirtualStore(new FramedClock()).GetVirtual(10000, "virtual"); var mockWorkingBeatmap = new Mock(); mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap); mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack); + mockWorkingBeatmap.SetupGet(w => w.Storyboard).Returns(storyboard); return mockWorkingBeatmap; } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index d9a675fd1713..2f768b6ffab3 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Storyboards; namespace osu.Game.Rulesets.Edit.Checks { @@ -15,6 +16,7 @@ public class CheckUnusedAudioAtEnd : ICheck public IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateUnusedAudioAtEnd(this), + new IssueTemplateUnusedAudioAtEndStoryboardOrVideo(this), }; public IEnumerable Run(BeatmapVerifierContext context) @@ -27,8 +29,31 @@ public IEnumerable Run(BeatmapVerifierContext context) if (mappedPercentage < 80) { double percentageLeft = Math.Abs(mappedPercentage - 100); - yield return new IssueTemplateUnusedAudioAtEnd(this).Create(percentageLeft); + + bool storyboardIsPresent = isAnyStoryboardElementPresent(context.WorkingBeatmap.Storyboard); + + if (storyboardIsPresent) + { + yield return new IssueTemplateUnusedAudioAtEndStoryboardOrVideo(this).Create(percentageLeft); + } + else + { + yield return new IssueTemplateUnusedAudioAtEnd(this).Create(percentageLeft); + } + } + } + + private bool isAnyStoryboardElementPresent(Storyboard storyboard) + { + foreach (var layer in storyboard.Layers) + { + foreach (var _ in layer.Elements) + { + return true; + } } + + return false; } public class IssueTemplateUnusedAudioAtEnd : IssueTemplate @@ -40,5 +65,15 @@ public IssueTemplateUnusedAudioAtEnd(ICheck check) public Issue Create(double percentageLeft) => new Issue(this, percentageLeft); } + + public class IssueTemplateUnusedAudioAtEndStoryboardOrVideo : IssueTemplate + { + public IssueTemplateUnusedAudioAtEndStoryboardOrVideo(ICheck check) + : base(check, IssueType.Warning, "Currently there is {0}% unused audio at the end. Ensure the outro significantly contributes to the song, or is being occupied by the video or storyboard, otherwise cut the outro.") + { + } + + public Issue Create(double percentageLeft) => new Issue(this, percentageLeft); + } } } From a8ce6a0bba5c65ab12360191d7589365d0996133 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Mar 2024 02:43:34 +0300 Subject: [PATCH 061/106] Use `Slice` method instead of index range operators for readability --- .../Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 2b058d5e1f89..283a59b7edec 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -303,16 +303,16 @@ private PathControlPoint[] convertPathString(string pointString, Vector2 offset) { int startIndex = segmentsBuffer[i].StartIndex; int endIndex = segmentsBuffer[i + 1].StartIndex; - controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints[startIndex..endIndex], pointsBuffer[endIndex])); + controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints.Slice(startIndex, endIndex - startIndex), pointsBuffer[endIndex])); } else { int startIndex = segmentsBuffer[i].StartIndex; - controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints[startIndex..], null)); + controlPoints.AddRange(convertPoints(segmentsBuffer[i].Type, allPoints.Slice(startIndex), null)); } } - return mergePointsLists(controlPoints); + return mergeControlPointsLists(controlPoints); } finally { @@ -402,7 +402,7 @@ static bool isLinear(Vector2 p0, Vector2 p1, Vector2 p2) - (p1.X - p0.X) * (p2.Y - p0.Y)); } - private PathControlPoint[] mergePointsLists(List> controlPointList) + private PathControlPoint[] mergeControlPointsLists(List> controlPointList) { int totalCount = 0; From af713a78695f4abd3e8023a98245f034fbb55da8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 19 Mar 2024 17:20:59 +0900 Subject: [PATCH 062/106] Fix incorrectly encoded score IsPerfect value --- .../Formats/LegacyScoreEncoderTest.cs | 38 ++++++++++++++++--- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 7 +++- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs index c0a7285f39bd..c0bf47dfc9df 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; @@ -21,9 +22,9 @@ public class LegacyScoreEncoderTest public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount) { var ruleset = new CatchRuleset().RulesetInfo; - var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); var beatmap = new TestBeatmap(ruleset); + scoreInfo.Statistics = new Dictionary { [HitResult.Great] = 50, @@ -31,14 +32,41 @@ public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCou [HitResult.Miss] = missCount, [HitResult.LargeTickMiss] = largeTickMissCount }; - var score = new Score { ScoreInfo = scoreInfo }; - var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); + var score = new Score { ScoreInfo = scoreInfo }; + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap, out _); Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount)); } - private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap) + [Test] + public void ScoreWithMissIsNotPerfect() + { + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset); + + scoreInfo.Statistics = new Dictionary + { + [HitResult.Great] = 2, + [HitResult.Miss] = 1, + }; + + scoreInfo.MaximumStatistics = new Dictionary + { + [HitResult.Great] = 3 + }; + + // Hit -> Miss -> Hit + scoreInfo.Combo = 1; + scoreInfo.MaxCombo = 1; + + encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, new Score { ScoreInfo = scoreInfo }, beatmap, out var decoder); + + Assert.That(decoder.DecodedPerfectValue, Is.False); + } + + private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap, out LegacyScoreDecoderTest.TestLegacyScoreDecoder decoder) { var encodeStream = new MemoryStream(); @@ -47,7 +75,7 @@ private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatm var decodeStream = new MemoryStream(encodeStream.GetBuffer()); - var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion); + decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion); var decodedAfterEncode = decoder.Parse(decodeStream); return decodedAfterEncode; } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 65e2c026550b..f2c096da1581 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -27,6 +27,11 @@ namespace osu.Game.Scoring.Legacy { public abstract class LegacyScoreDecoder { + /// + /// The decoded "IsPerfect" value. This isn't used by osu!lazer. + /// + public bool DecodedPerfectValue { get; private set; } + private IBeatmap currentBeatmap; private Ruleset currentRuleset; @@ -82,7 +87,7 @@ public Score Parse(Stream stream) scoreInfo.MaxCombo = sr.ReadUInt16(); /* score.Perfect = */ - sr.ReadBoolean(); + DecodedPerfectValue = sr.ReadBoolean(); scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 4ee42319254e..1df54565e95e 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -93,7 +93,7 @@ public void Encode(Stream stream, bool leaveOpen = false) sw.Write((ushort)(score.ScoreInfo.GetCountMiss() ?? 0)); sw.Write((int)(score.ScoreInfo.TotalScore)); sw.Write((ushort)score.ScoreInfo.MaxCombo); - sw.Write(score.ScoreInfo.Combo == score.ScoreInfo.MaxCombo); + sw.Write(score.ScoreInfo.Combo == score.ScoreInfo.GetMaximumAchievableCombo()); sw.Write((int)score.ScoreInfo.Ruleset.CreateInstance().ConvertToLegacyMods(score.ScoreInfo.Mods)); sw.Write(getHpGraphFormatted()); From 6e3350941749a87b21ed2c819c063e7a556509f0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 19 Mar 2024 17:44:37 +0900 Subject: [PATCH 063/106] Remove added property, use local decoding instead --- .../Formats/LegacyScoreEncoderTest.cs | 34 ++++++++++++++++--- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 7 +--- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs index c0bf47dfc9df..806f538249fa 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs @@ -5,6 +5,7 @@ using System.IO; using NUnit.Framework; using osu.Game.Beatmaps.Formats; +using osu.Game.IO.Legacy; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; @@ -34,7 +35,7 @@ public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCou }; var score = new Score { ScoreInfo = scoreInfo }; - var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap, out _); + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount)); } @@ -61,12 +62,35 @@ public void ScoreWithMissIsNotPerfect() scoreInfo.Combo = 1; scoreInfo.MaxCombo = 1; - encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, new Score { ScoreInfo = scoreInfo }, beatmap, out var decoder); + using (var ms = new MemoryStream()) + { + new LegacyScoreEncoder(new Score { ScoreInfo = scoreInfo }, beatmap).Encode(ms, true); + + ms.Seek(0, SeekOrigin.Begin); + + using (var sr = new SerializationReader(ms)) + { + sr.ReadByte(); // ruleset id + sr.ReadInt32(); // version + sr.ReadString(); // beatmap hash + sr.ReadString(); // username + sr.ReadString(); // score hash + sr.ReadInt16(); // count300 + sr.ReadInt16(); // count100 + sr.ReadInt16(); // count50 + sr.ReadInt16(); // countGeki + sr.ReadInt16(); // countKatu + sr.ReadInt16(); // countMiss + sr.ReadInt32(); // total score + sr.ReadInt16(); // max combo + bool isPerfect = sr.ReadBoolean(); // full combo - Assert.That(decoder.DecodedPerfectValue, Is.False); + Assert.That(isPerfect, Is.False); + } + } } - private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap, out LegacyScoreDecoderTest.TestLegacyScoreDecoder decoder) + private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap) { var encodeStream = new MemoryStream(); @@ -75,7 +99,7 @@ private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatm var decodeStream = new MemoryStream(encodeStream.GetBuffer()); - decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion); + var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion); var decodedAfterEncode = decoder.Parse(decodeStream); return decodedAfterEncode; } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index f2c096da1581..65e2c026550b 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -27,11 +27,6 @@ namespace osu.Game.Scoring.Legacy { public abstract class LegacyScoreDecoder { - /// - /// The decoded "IsPerfect" value. This isn't used by osu!lazer. - /// - public bool DecodedPerfectValue { get; private set; } - private IBeatmap currentBeatmap; private Ruleset currentRuleset; @@ -87,7 +82,7 @@ public Score Parse(Stream stream) scoreInfo.MaxCombo = sr.ReadUInt16(); /* score.Perfect = */ - DecodedPerfectValue = sr.ReadBoolean(); + sr.ReadBoolean(); scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); From f6069d8d93b05c752b1aeaa36e4c269580880f26 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 19 Mar 2024 17:46:18 +0900 Subject: [PATCH 064/106] Compare against MaxCombo instead --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 1df54565e95e..93f51ee74dda 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -93,7 +93,7 @@ public void Encode(Stream stream, bool leaveOpen = false) sw.Write((ushort)(score.ScoreInfo.GetCountMiss() ?? 0)); sw.Write((int)(score.ScoreInfo.TotalScore)); sw.Write((ushort)score.ScoreInfo.MaxCombo); - sw.Write(score.ScoreInfo.Combo == score.ScoreInfo.GetMaximumAchievableCombo()); + sw.Write(score.ScoreInfo.MaxCombo == score.ScoreInfo.GetMaximumAchievableCombo()); sw.Write((int)score.ScoreInfo.Ruleset.CreateInstance().ConvertToLegacyMods(score.ScoreInfo.Mods)); sw.Write(getHpGraphFormatted()); From 0de5ca8d2df633b044b41d121bb3ed97aee73e47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Mar 2024 01:26:39 +0800 Subject: [PATCH 065/106] Update incorrect xmldoc --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index fd1ce5d82922..8bb3ae8182a1 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play { /// - /// Simple that resumes after 800ms. + /// Simple that resumes after a short delay. /// public partial class DelayedResumeOverlay : ResumeOverlay { From 0211ae12adbed9b0d37881fbc87774ca901a0953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Mar 2024 19:16:33 +0100 Subject: [PATCH 066/106] Add failing test case for crash on empty beatmap --- .../Editing/Checks/CheckUnusedAudioAtEndTest.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs index 33d73a808644..bf996b06ea83 100644 --- a/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckUnusedAudioAtEndTest.cs @@ -55,6 +55,16 @@ public void Setup() }; } + [Test] + public void TestEmptyBeatmap() + { + var context = getContext(new Beatmap()); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckUnusedAudioAtEnd.IssueTemplateUnusedAudioAtEnd); + } + [Test] public void TestAudioNotFullyUsed() { From 2b83e6bc4cc325375f1db483e84af25f73330087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Mar 2024 19:17:22 +0100 Subject: [PATCH 067/106] Fix check crash on empty beatmap --- osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs index 2f768b6ffab3..2e97fbeb9929 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckUnusedAudioAtEnd.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Storyboards; @@ -21,7 +22,7 @@ public class CheckUnusedAudioAtEnd : ICheck public IEnumerable Run(BeatmapVerifierContext context) { - double mappedLength = context.Beatmap.GetLastObjectTime(); + double mappedLength = context.Beatmap.HitObjects.Any() ? context.Beatmap.GetLastObjectTime() : 0; double trackLength = context.WorkingBeatmap.Track.Length; double mappedPercentage = Math.Round(mappedLength / trackLength * 100); From e4418547feba011e5461d2a9d5358d198d341427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Mar 2024 19:19:07 +0100 Subject: [PATCH 068/106] Document `GetLastObjectTime()` exception on empty beatmap --- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index b5bb6ccafc6a..6fe494ca0f04 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; @@ -129,6 +130,7 @@ static void addCombo(HitObject hitObject, ref int combo) /// /// It's not super efficient so calls should be kept to a minimum. /// + /// If has no objects. public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime()); #region Helper methods From 808d6e09436468c8690311f235852ed0dd7834c9 Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 19 Mar 2024 16:02:06 -0400 Subject: [PATCH 069/106] Prevent potential threading issues --- osu.Desktop/DiscordRichPresence.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index ca26cab0fddd..080032a298eb 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -97,11 +97,13 @@ private void load(OsuConfigManager config) private void onReady(object _, ReadyMessage __) { Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); - updateStatus(); + Schedule(updateStatus); } private void updateStatus() { + Debug.Assert(ThreadSafety.IsUpdateThread); + if (!client.IsInitialized) return; From 0ecfa580d7a48d247ab9fa9907fdd0f1d739d732 Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 19 Mar 2024 16:03:32 -0400 Subject: [PATCH 070/106] Move room code from activity code, prevent duplicate RPC updates --- osu.Desktop/DiscordRichPresence.cs | 64 +++++++++++++++++------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 080032a298eb..633b6324b7b0 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Text; using DiscordRPC; using DiscordRPC.Message; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game; @@ -48,6 +50,8 @@ internal partial class DiscordRichPresence : Component private readonly Bindable privacyMode = new Bindable(); + private int usersCurrentlyInLobby = 0; + private readonly RichPresence presence = new RichPresence { Assets = new Assets { LargeImageKey = "osu_logo_lazer" }, @@ -115,10 +119,10 @@ private void updateStatus() Logger.Log("Updating Discord RPC", LoggingTarget.Network, LogLevel.Debug); + bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; + if (activity.Value != null) { - bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; - presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); @@ -137,38 +141,44 @@ private void updateStatus() { presence.Buttons = null; } + } + else + { + presence.State = "Idle"; + presence.Details = string.Empty; + } - if (!hideIdentifiableInformation && multiplayerClient.Room != null) - { - MultiplayerRoom room = multiplayerClient.Room; - presence.Party = new Party - { - Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private, - ID = room.RoomID.ToString(), - // technically lobbies can have infinite users, but Discord needs this to be set to something. - // to make party display sensible, assign a powers of two above participants count (8 at minimum). - Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))), - Size = room.Users.Count, - }; + if (!hideIdentifiableInformation && multiplayerClient.Room != null) + { + MultiplayerRoom room = multiplayerClient.Room; - RoomSecret roomSecret = new RoomSecret - { - RoomID = room.RoomID, - Password = room.Settings.Password, - }; + if (room.Users.Count == usersCurrentlyInLobby) + return; - presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); - } - else + presence.Party = new Party { - presence.Party = null; - presence.Secrets.JoinSecret = null; - } + Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private, + ID = room.RoomID.ToString(), + // technically lobbies can have infinite users, but Discord needs this to be set to something. + // to make party display sensible, assign a powers of two above participants count (8 at minimum). + Max = (int)Math.Max(8, Math.Pow(2, Math.Ceiling(Math.Log2(room.Users.Count)))), + Size = room.Users.Count, + }; + + RoomSecret roomSecret = new RoomSecret + { + RoomID = room.RoomID, + Password = room.Settings.Password, + }; + + presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); + usersCurrentlyInLobby = room.Users.Count; } else { - presence.State = "Idle"; - presence.Details = string.Empty; + presence.Party = null; + presence.Secrets.JoinSecret = null; + usersCurrentlyInLobby = 0; } // update user information From c71daba4f663164a8180561bbadc8e0ee6f78a46 Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 19 Mar 2024 16:05:13 -0400 Subject: [PATCH 071/106] Improve logging of RPC Co-authored-by: Salman Ahmed --- osu.Desktop/DiscordRichPresence.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 633b6324b7b0..f47b2eaba553 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -117,8 +117,6 @@ private void updateStatus() return; } - Logger.Log("Updating Discord RPC", LoggingTarget.Network, LogLevel.Debug); - bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; if (activity.Value != null) @@ -181,6 +179,8 @@ private void updateStatus() usersCurrentlyInLobby = 0; } + Logger.Log($"Updating Discord RPC presence with activity status: {presence.State}, details: {presence.Details}", LoggingTarget.Network, LogLevel.Debug); + // update user information if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; From 4305c3db5b70b70d1e28ba1deeb807b28742481e Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 19 Mar 2024 16:15:22 -0400 Subject: [PATCH 072/106] Show login overlay when joining room while not logged in --- osu.Desktop/DiscordRichPresence.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index f47b2eaba553..a2d7ace0e0aa 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -19,6 +19,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Users; using LogLevel = osu.Framework.Logging.LogLevel; @@ -42,6 +43,8 @@ internal partial class DiscordRichPresence : Component [Resolved] private OsuGame game { get; set; } = null!; + private LoginOverlay? login { get; set; } + [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; @@ -65,6 +68,8 @@ internal partial class DiscordRichPresence : Component [BackgroundDependencyLoader] private void load(OsuConfigManager config) { + login = game.Dependencies.Get(); + client = new DiscordRpcClient(client_id) { SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady. @@ -203,6 +208,12 @@ private void onJoin(object sender, JoinMessage args) { game.Window?.Raise(); + if (!api.IsLoggedIn) + { + Schedule(() => login?.Show()); + return; + } + Logger.Log($"Received room secret from Discord RPC Client: \"{args.Secret}\"", LoggingTarget.Network, LogLevel.Debug); // Stable and lazer share the same Discord client ID, meaning they can accept join requests from each other. From 1a08dbaa2ba929c26e3463163f3c8ac9523809c5 Mon Sep 17 00:00:00 2001 From: jvyden Date: Tue, 19 Mar 2024 17:03:30 -0400 Subject: [PATCH 073/106] Fix code style --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index a2d7ace0e0aa..d8013aabfe7d 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -53,7 +53,7 @@ internal partial class DiscordRichPresence : Component private readonly Bindable privacyMode = new Bindable(); - private int usersCurrentlyInLobby = 0; + private int usersCurrentlyInLobby; private readonly RichPresence presence = new RichPresence { From d83a53fc944eea90ad8244e248ec762e6d6d6ad3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Mar 2024 12:10:05 +0800 Subject: [PATCH 074/106] Remove unused `ScreenBreadcrumbControl` See https://github.com/ppy/osu-framework/pull/6218#discussion_r1529932798. --- .../TestSceneScreenBreadcrumbControl.cs | 138 ------------------ .../UserInterface/ScreenBreadcrumbControl.cs | 48 ------ 2 files changed, 186 deletions(-) delete mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs delete mode 100644 osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs deleted file mode 100644 index 968cf9f9db59..000000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenBreadcrumbControl.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Screens; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Screens; -using osuTK; - -namespace osu.Game.Tests.Visual.UserInterface -{ - [TestFixture] - public partial class TestSceneScreenBreadcrumbControl : OsuTestScene - { - private readonly ScreenBreadcrumbControl breadcrumbs; - private readonly OsuScreenStack screenStack; - - public TestSceneScreenBreadcrumbControl() - { - OsuSpriteText titleText; - - IScreen startScreen = new TestScreenOne(); - - screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }; - screenStack.Push(startScreen); - - Children = new Drawable[] - { - screenStack, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), - Children = new Drawable[] - { - breadcrumbs = new ScreenBreadcrumbControl(screenStack) - { - RelativeSizeAxes = Axes.X, - }, - titleText = new OsuSpriteText(), - }, - }, - }; - - breadcrumbs.Current.ValueChanged += screen => titleText.Text = $"Changed to {screen.NewValue}"; - breadcrumbs.Current.TriggerChange(); - - waitForCurrent(); - pushNext(); - waitForCurrent(); - pushNext(); - waitForCurrent(); - - AddStep(@"make start current", () => startScreen.MakeCurrent()); - - waitForCurrent(); - pushNext(); - waitForCurrent(); - AddAssert(@"only 2 items", () => breadcrumbs.Items.Count == 2); - AddStep(@"exit current", () => screenStack.CurrentScreen.Exit()); - AddAssert(@"current screen is first", () => startScreen == screenStack.CurrentScreen); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - breadcrumbs.StripColour = colours.Blue; - } - - private void pushNext() => AddStep(@"push next screen", () => ((TestScreen)screenStack.CurrentScreen).PushNext()); - private void waitForCurrent() => AddUntilStep("current screen", () => screenStack.CurrentScreen.IsCurrentScreen()); - - private abstract partial class TestScreen : OsuScreen - { - protected abstract string NextTitle { get; } - protected abstract TestScreen CreateNextScreen(); - - public TestScreen PushNext() - { - TestScreen screen = CreateNextScreen(); - this.Push(screen); - - return screen; - } - - protected TestScreen() - { - InternalChild = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), - Children = new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = Title, - }, - new RoundedButton - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Width = 100, - Text = $"Push {NextTitle}", - Action = () => PushNext(), - }, - }, - }; - } - } - - private partial class TestScreenOne : TestScreen - { - public override string Title => @"Screen One"; - protected override string NextTitle => @"Two"; - protected override TestScreen CreateNextScreen() => new TestScreenTwo(); - } - - private partial class TestScreenTwo : TestScreen - { - public override string Title => @"Screen Two"; - protected override string NextTitle => @"One"; - protected override TestScreen CreateNextScreen() => new TestScreenOne(); - } - } -} diff --git a/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs deleted file mode 100644 index 65dce422d66f..000000000000 --- a/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Screens; - -namespace osu.Game.Graphics.UserInterface -{ - /// - /// A which follows the active screen (and allows navigation) in a stack. - /// - public partial class ScreenBreadcrumbControl : BreadcrumbControl - { - public ScreenBreadcrumbControl(ScreenStack stack) - { - stack.ScreenPushed += onPushed; - stack.ScreenExited += onExited; - - if (stack.CurrentScreen != null) - onPushed(null, stack.CurrentScreen); - } - - protected override void SelectTab(TabItem tab) - { - // override base method to prevent current item from being changed on click. - // depend on screen push/exit to change current item instead. - tab.Value.MakeCurrent(); - } - - private void onPushed(IScreen lastScreen, IScreen newScreen) - { - AddItem(newScreen); - Current.Value = newScreen; - } - - private void onExited(IScreen lastScreen, IScreen newScreen) - { - if (newScreen != null) - Current.Value = newScreen; - - Items.ToList().SkipWhile(s => s != Current.Value).Skip(1).ForEach(RemoveItem); - } - } -} From b11ae1c5714b43d33ebbec569faf6569d0304c25 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 20 Mar 2024 06:40:18 +0300 Subject: [PATCH 075/106] Organise code, still hook up to `RoomChanged` to update room privacy mode, and use `SkipIdenticalPresence` + scheduling to avoid potential rate-limits --- osu.Desktop/DiscordRichPresence.cs | 85 +++++++++++++++++++----------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index d8013aabfe7d..8e4af5c5b17a 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -2,16 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Text; using DiscordRPC; using DiscordRPC.Message; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Development; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; +using osu.Framework.Threading; using osu.Game; using osu.Game.Configuration; using osu.Game.Extensions; @@ -53,8 +53,6 @@ internal partial class DiscordRichPresence : Component private readonly Bindable privacyMode = new Bindable(); - private int usersCurrentlyInLobby; - private readonly RichPresence presence = new RichPresence { Assets = new Assets { LargeImageKey = "osu_logo_lazer" }, @@ -72,7 +70,9 @@ private void load(OsuConfigManager config) client = new DiscordRpcClient(client_id) { - SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady. + // SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation + // to check whether a difference has actually occurred before sending a command to Discord (with a minor caveat that's handled in onReady). + SkipIdenticalPresence = true }; client.OnReady += onReady; @@ -95,10 +95,11 @@ private void load(OsuConfigManager config) activity.BindTo(u.NewValue.Activity); }, true); - ruleset.BindValueChanged(_ => updateStatus()); - status.BindValueChanged(_ => updateStatus()); - activity.BindValueChanged(_ => updateStatus()); - privacyMode.BindValueChanged(_ => updateStatus()); + ruleset.BindValueChanged(_ => updatePresence()); + status.BindValueChanged(_ => updatePresence()); + activity.BindValueChanged(_ => updatePresence()); + privacyMode.BindValueChanged(_ => updatePresence()); + multiplayerClient.RoomUpdated += onRoomUpdated; client.Initialize(); } @@ -106,24 +107,44 @@ private void load(OsuConfigManager config) private void onReady(object _, ReadyMessage __) { Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); - Schedule(updateStatus); + + // when RPC is lost and reconnected, we have to clear presence state for updatePresence to work (see DiscordRpcClient.SkipIdenticalPresence). + if (client.CurrentPresence != null) + client.SetPresence(null); + + updatePresence(); } - private void updateStatus() - { - Debug.Assert(ThreadSafety.IsUpdateThread); + private void onRoomUpdated() => updatePresence(); - if (!client.IsInitialized) - return; + private ScheduledDelegate? presenceUpdateDelegate; - if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) + private void updatePresence() + { + presenceUpdateDelegate?.Cancel(); + presenceUpdateDelegate = Scheduler.AddDelayed(() => { - client.ClearPresence(); - return; - } + if (!client.IsInitialized) + return; + + if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) + { + client.ClearPresence(); + return; + } + + bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; - bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; + updatePresenceStatus(hideIdentifiableInformation); + updatePresenceParty(hideIdentifiableInformation); + updatePresenceAssets(); + client.SetPresence(presence); + }, 200); + } + + private void updatePresenceStatus(bool hideIdentifiableInformation) + { if (activity.Value != null) { presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); @@ -150,14 +171,14 @@ private void updateStatus() presence.State = "Idle"; presence.Details = string.Empty; } + } + private void updatePresenceParty(bool hideIdentifiableInformation) + { if (!hideIdentifiableInformation && multiplayerClient.Room != null) { MultiplayerRoom room = multiplayerClient.Room; - if (room.Users.Count == usersCurrentlyInLobby) - return; - presence.Party = new Party { Privacy = string.IsNullOrEmpty(room.Settings.Password) ? Party.PrivacySetting.Public : Party.PrivacySetting.Private, @@ -175,17 +196,16 @@ private void updateStatus() }; presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); - usersCurrentlyInLobby = room.Users.Count; } else { presence.Party = null; presence.Secrets.JoinSecret = null; - usersCurrentlyInLobby = 0; } + } - Logger.Log($"Updating Discord RPC presence with activity status: {presence.State}, details: {presence.Details}", LoggingTarget.Network, LogLevel.Debug); - + private void updatePresenceAssets() + { // update user information if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; @@ -200,17 +220,15 @@ private void updateStatus() // update ruleset presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; presence.Assets.SmallImageText = ruleset.Value.Name; - - client.SetPresence(presence); } - private void onJoin(object sender, JoinMessage args) + private void onJoin(object sender, JoinMessage args) => Scheduler.AddOnce(() => { game.Window?.Raise(); if (!api.IsLoggedIn) { - Schedule(() => login?.Show()); + login?.Show(); return; } @@ -231,7 +249,7 @@ private void onJoin(object sender, JoinMessage args) }); request.Failure += _ => Logger.Log($"Could not join multiplayer room, room could not be found (room ID: {roomId}).", LoggingTarget.Network, LogLevel.Important); api.Queue(request); - } + }); private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' }); @@ -294,6 +312,9 @@ private static bool tryParseRoomSecret(string secretJson, out long roomId, out s protected override void Dispose(bool isDisposing) { + if (multiplayerClient.IsNotNull()) + multiplayerClient.RoomUpdated -= onRoomUpdated; + client.Dispose(); base.Dispose(isDisposing); } From 5f86b5a2fa2b109ec63d283a84356dc46efb67ac Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 20 Mar 2024 07:36:15 +0300 Subject: [PATCH 076/106] Use DI correctly --- osu.Desktop/DiscordRichPresence.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 8e4af5c5b17a..d78459ff28d4 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -43,6 +43,7 @@ internal partial class DiscordRichPresence : Component [Resolved] private OsuGame game { get; set; } = null!; + [Resolved] private LoginOverlay? login { get; set; } [Resolved] @@ -66,8 +67,6 @@ internal partial class DiscordRichPresence : Component [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - login = game.Dependencies.Get(); - client = new DiscordRpcClient(client_id) { // SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation From fd509c82f50a59293fee0978769bcaa02c28f852 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Mar 2024 12:52:54 +0800 Subject: [PATCH 077/106] Adjust code structure slightly --- .../Timeline/TimelineControlPointDisplay.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 8e522fa715f6..116a3ee1050c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -32,10 +32,7 @@ protected override void LoadBeatmap(EditorBeatmap beatmap) controlPointGroups.UnbindAll(); controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((_, _) => - { - invalidateGroups(); - }, true); + controlPointGroups.BindCollectionChanged((_, _) => groupCache.Invalidate(), true); } protected override void Update() @@ -51,19 +48,20 @@ protected override void Update() if (visibleRange != newRange) { visibleRange = newRange; - invalidateGroups(); + groupCache.Invalidate(); } if (!groupCache.IsValid) + { recreateDrawableGroups(); + groupCache.Validate(); + } } - private void invalidateGroups() => groupCache.Invalidate(); - private void recreateDrawableGroups() { // Remove groups outside the visible range - foreach (var drawableGroup in this) + foreach (TimelineControlPointGroup drawableGroup in this) { if (!shouldBeVisible(drawableGroup.Group)) drawableGroup.Expire(); @@ -93,8 +91,6 @@ private void recreateDrawableGroups() Add(new TimelineControlPointGroup(group)); } - - groupCache.Validate(); } private bool shouldBeVisible(ControlPointGroup group) => group.Time >= visibleRange.min && group.Time <= visibleRange.max; From 1f343b75454c416b8c17d98428330096213c7fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 20 Mar 2024 08:27:49 +0100 Subject: [PATCH 078/106] Add extended logging when discarding online metadata lookup results Related to: https://github.com/ppy/osu/issues/27674 Relevant log output for that particular case: [network] 2024-03-20 07:25:30 [verbose]: Performing request osu.Game.Online.API.Requests.GetBeatmapRequest [network] 2024-03-20 07:25:30 [verbose]: Request to https://dev.ppy.sh/api/v2/beatmaps/lookup successfully completed! [network] 2024-03-20 07:25:30 [verbose]: GetBeatmapRequest finished with response size of 3,170 bytes [database] 2024-03-20 07:25:30 [verbose]: [4fe02] [APIBeatmapMetadataSource] Online retrieval mapped Tsukiyama Sae - Hana Saku Iro wa Koi no Gotoshi (Log Off Now) [Destiny] to 744883 / 1613507. [database] 2024-03-20 07:25:30 [verbose]: Discarding metadata lookup result due to mismatching online ID (expected: 1570982 actual: 1613507) [network] 2024-03-20 07:25:30 [verbose]: Performing request osu.Game.Online.API.Requests.GetBeatmapRequest [network] 2024-03-20 07:25:30 [verbose]: Request to https://dev.ppy.sh/api/v2/beatmaps/lookup successfully completed! [network] 2024-03-20 07:25:30 [verbose]: GetBeatmapRequest finished with response size of 2,924 bytes [database] 2024-03-20 07:25:30 [verbose]: [4fe02] [APIBeatmapMetadataSource] Online retrieval mapped Tsukiyama Sae - Hana Saku Iro wa Koi no Gotoshi (Log Off Now) [Easy] to 744883 / 1570982. [database] 2024-03-20 07:25:30 [verbose]: Discarding metadata lookup result due to mismatching online ID (expected: 1613507 actual: 1570982) Note that the online IDs are swapped. --- osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs b/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs index f395718a9334..034ec31ee493 100644 --- a/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs +++ b/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Online.API; @@ -85,10 +86,16 @@ public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch) private bool shouldDiscardLookupResult(OnlineBeatmapMetadata result, BeatmapInfo beatmapInfo) { if (beatmapInfo.OnlineID > 0 && result.BeatmapID != beatmapInfo.OnlineID) + { + Logger.Log($"Discarding metadata lookup result due to mismatching online ID (expected: {beatmapInfo.OnlineID} actual: {result.BeatmapID})", LoggingTarget.Database); return true; + } if (beatmapInfo.OnlineID == -1 && result.MD5Hash != beatmapInfo.MD5Hash) + { + Logger.Log($"Discarding metadata lookup result due to mismatching hash (expected: {beatmapInfo.MD5Hash} actual: {result.MD5Hash})", LoggingTarget.Database); return true; + } return false; } From c78e203df5434495a79589559b973917a4088a3e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 20 Mar 2024 17:30:34 +0900 Subject: [PATCH 079/106] Fix infinite health processor loop when no top-level objects This is unlikely to occur in actual gameplay, but occurs in the follow-up test. --- osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs index 2bc3ea80ec74..7cee5ebecf09 100644 --- a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs @@ -108,6 +108,9 @@ protected override double ComputeDrainRate() increaseHp(h); } + if (topLevelObjectCount == 0) + return testDrop; + if (!fail && currentHp < lowestHpEnd) { fail = true; From 66ace02e5872bd900acdaf7682a9178eea7dc168 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 20 Mar 2024 17:31:11 +0900 Subject: [PATCH 080/106] Add test for banana shower fail --- osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs index d0a8ce4bbc98..1b46be01fb69 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs @@ -19,6 +19,7 @@ public class CatchHealthProcessorTest [new Droplet(), 0.01, true], [new TinyDroplet(), 0, false], [new Banana(), 0, false], + [new BananaShower(), 0, false] ]; [TestCaseSource(nameof(test_cases))] From bf5640049a73e2eb01c5cc02814aaea43d7c00e5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 20 Mar 2024 17:31:31 +0900 Subject: [PATCH 081/106] Fix banana showers causing fails when hp is at 0 --- osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index 2f55f9a85fa8..b2509091fe2e 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -32,6 +32,10 @@ protected override bool CheckDefaultFailCondition(JudgementResult result) if (result.Type == HitResult.SmallTickMiss) return false; + // on stable, banana showers don't exist as concrete objects themselves, so they can't cause a fail. + if (result.HitObject is BananaShower) + return false; + return base.CheckDefaultFailCondition(result); } From 4904c49c38b418f3a151c34c114be96983bb03b7 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 20 Mar 2024 09:31:58 -0300 Subject: [PATCH 082/106] Add abnormal difficulty settings checks --- .../CheckAbnormalDifficultySettingsTest.cs | 93 +++++++++++++++++++ osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 5 +- .../Checks/CheckAbnormalDifficultySettings.cs | 82 ++++++++++++++++ 3 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs b/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs new file mode 100644 index 000000000000..94305755c443 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Edit.Checks; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Tests.Beatmaps; +using System.Linq; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckAbnormalDifficultySettingsTest + { + private CheckAbnormalDifficultySettings check = null!; + + private IBeatmap beatmap = new Beatmap(); + + [SetUp] + public void Setup() + { + check = new CheckAbnormalDifficultySettings(); + beatmap.Difficulty = new(); + } + + [Test] + public void TestSettingsNormal() + { + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestAllSettingsMoreThanOneDecimal() + { + beatmap.Difficulty = new() + { + ApproachRate = 5.55f, + OverallDifficulty = 7.7777f, + CircleSize = 4.444f, + DrainRate = 1.1111111111f, + }; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(4)); + } + + [Test] + public void TestAllSettingsLessThanZero() + { + beatmap.Difficulty = new() + { + ApproachRate = -1, + OverallDifficulty = -20, + CircleSize = -11, + DrainRate = -34, + }; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(4)); + } + + [Test] + public void TestAllSettingsHigherThanTen() + { + beatmap.Difficulty = new() + { + ApproachRate = 14, + OverallDifficulty = 24, + CircleSize = 30, + DrainRate = 90, + }; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(4)); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index dcf5eb4da9cd..edabe941b7c8 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -41,7 +41,10 @@ public class BeatmapVerifier : IBeatmapVerifier new CheckPreviewTime(), // Events - new CheckBreaks() + new CheckBreaks(), + + // Settings + new CheckAbnormalDifficultySettings(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs new file mode 100644 index 000000000000..73049b323d8b --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs @@ -0,0 +1,82 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Edit.Checks.Components; + + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckAbnormalDifficultySettings : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Abnormal difficulty settings"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateMoreThanOneDecimal(this), + new IssueTemplateOutOfRange(this), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (hasMoreThanOneDecimalPlace(diff.ApproachRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); + + if (isOutOfRange(diff.ApproachRate)) + yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); + + + if (hasMoreThanOneDecimalPlace(diff.OverallDifficulty)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (isOutOfRange(diff.OverallDifficulty)) + yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + + + if (hasMoreThanOneDecimalPlace(diff.CircleSize)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); + + if (isOutOfRange(diff.CircleSize)) + yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); + + + if (hasMoreThanOneDecimalPlace(diff.DrainRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + + if (isOutOfRange(diff.DrainRate)) + yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + } + + private bool isOutOfRange(float setting) + { + return setting < 0f || setting > 10f; + } + + private bool hasMoreThanOneDecimalPlace(float setting) + { + return float.Round(setting, 1) != setting; + } + + public class IssueTemplateMoreThanOneDecimal : IssueTemplate + { + public IssueTemplateMoreThanOneDecimal(ICheck check) + : base(check, IssueType.Problem, "{0} {1} has more than one decimal place.") + { + } + + public Issue Create(string settingName, float settingValue) => new Issue(this, settingName, settingValue); + } + + public class IssueTemplateOutOfRange : IssueTemplate + { + public IssueTemplateOutOfRange(ICheck check) + : base(check, IssueType.Warning, "{0} is {1} although it is capped between 0 to 10 in-game.") + { + } + + public Issue Create(string settingName, float settingValue) => new Issue(this, settingName, settingValue); + } + } +} From ac7fca10d63e3f92db65572ea77ae0ab4bb58df6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 21 Mar 2024 00:47:37 +0900 Subject: [PATCH 083/106] Warn about not using an official "deployed" build --- osu.Game/Localisation/NotificationsStrings.cs | 5 +++++ osu.Game/Updater/UpdateManager.cs | 6 ++++++ osu.Game/Utils/OfficialBuildAttribute.cs | 12 ++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 osu.Game/Utils/OfficialBuildAttribute.cs diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index f4965e4ebed0..5328bcd066ed 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -125,6 +125,11 @@ public static class NotificationsStrings /// public static LocalisableString UpdateReadyToInstall => new TranslatableString(getKey(@"update_ready_to_install"), @"Update ready to install. Click to restart!"); + /// + /// "This is not an official build of the game and scores will not be submitted." + /// + public static LocalisableString NotOfficialBuild => new TranslatableString(getKey(@"not_official_build"), @"This is not an official build of the game and scores will not be submitted."); + /// /// "Downloading update..." /// diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 8f13e0f42afb..bcb28d8b14e8 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Reflection; using System.Threading.Tasks; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,6 +13,7 @@ using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Utils; using osuTK; namespace osu.Game.Updater @@ -51,6 +54,9 @@ protected override void LoadComplete() // only show a notification if we've previously saved a version to the config file (ie. not the first run). if (!string.IsNullOrEmpty(lastVersion)) Notifications.Post(new UpdateCompleteNotification(version)); + + if (RuntimeInfo.EntryAssembly.GetCustomAttribute() == null) + Notifications.Post(new SimpleNotification { Text = NotificationsStrings.NotOfficialBuild }); } // debug / local compilations will reset to a non-release string. diff --git a/osu.Game/Utils/OfficialBuildAttribute.cs b/osu.Game/Utils/OfficialBuildAttribute.cs new file mode 100644 index 000000000000..66c1ef5591e7 --- /dev/null +++ b/osu.Game/Utils/OfficialBuildAttribute.cs @@ -0,0 +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; +using JetBrains.Annotations; + +namespace osu.Game.Utils +{ + [UsedImplicitly] + [AttributeUsage(AttributeTargets.Assembly)] + public class OfficialBuildAttribute : Attribute; +} From c605e463a41388f43a0493d1246a813f1f7d4ab1 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 20 Mar 2024 15:52:16 -0300 Subject: [PATCH 084/106] Add mania keycount check --- .../Editor/Checks/CheckKeyCountTest.cs | 75 +++++++++++++++++++ .../Edit/Checks/CheckKeyCount.cs | 55 ++++++++++++++ .../Edit/ManiaBeatmapVerifier.cs | 24 ++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 + .../Checks/CheckAbnormalDifficultySettings.cs | 4 +- 5 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs create mode 100644 osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs create mode 100644 osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs new file mode 100644 index 000000000000..91361e2a625a --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; +using osu.Game.Rulesets.Mania.Edit.Checks; +using System.Linq; + +namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks +{ + [TestFixture] + public class CheckKeyCountTest + { + private CheckKeyCount check = null!; + + private IBeatmap beatmap = null!; + + [SetUp] + public void Setup() + { + check = new CheckKeyCount(); + + beatmap = new Beatmap() + { + BeatmapInfo = new BeatmapInfo + { + Ruleset = new ManiaRuleset().RulesetInfo + } + }; + } + + [Test] + public void TestKeycountFour() + { + beatmap.Difficulty.CircleSize = 4; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestKeycountSmallerThanFour() + { + beatmap.Difficulty.CircleSize = 1; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountTooLow); + } + + [Test] + public void TestKeycountHigherThanTen() + { + beatmap.Difficulty.CircleSize = 11; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountNonStandard); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs new file mode 100644 index 000000000000..57991bd3ffdf --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Mania.Edit.Checks +{ + public class CheckKeyCount : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Check mania keycount."); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateKeycountTooLow(this), + new IssueTemplateKeycountNonStandard(this), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (diff.CircleSize < 4) + { + yield return new IssueTemplateKeycountTooLow(this).Create(diff.CircleSize); + } + + if (diff.CircleSize > 10) + { + yield return new IssueTemplateKeycountNonStandard(this).Create(diff.CircleSize); + } + } + + public class IssueTemplateKeycountTooLow : IssueTemplate + { + public IssueTemplateKeycountTooLow(ICheck check) + : base(check, IssueType.Problem, "Key count is {0} and must be 4 or higher.") + { + } + + public Issue Create(float current) => new Issue(this, current); + } + + public class IssueTemplateKeycountNonStandard : IssueTemplate + { + public IssueTemplateKeycountNonStandard(ICheck check) + : base(check, IssueType.Warning, "Key count {0} is higher than 10.") + { + } + + public Issue Create(float current) => new Issue(this, current); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs new file mode 100644 index 000000000000..778dcfc6e12f --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Mania.Edit.Checks; + +namespace osu.Game.Rulesets.Mania.Edit +{ + public class ManiaBeatmapVerifier : IBeatmapVerifier + { + private readonly List checks = new List + { + new CheckKeyCount(), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + return checks.SelectMany(check => check.Run(context)); + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0b54fb3da0bf..3d4803f1e4d5 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -65,6 +65,8 @@ public class ManiaRuleset : Ruleset, ILegacyRuleset public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); + public override IBeatmapVerifier CreateBeatmapVerifier() => new ManiaBeatmapVerifier(); + public override ISkin? CreateSkinTransformer(ISkin skin, IBeatmap beatmap) { switch (skin) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs index 73049b323d8b..3c454a57fe11 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Edit.Checks.Components; - namespace osu.Game.Rulesets.Edit.Checks { public class CheckAbnormalDifficultySettings : ICheck @@ -20,6 +19,7 @@ public class CheckAbnormalDifficultySettings : ICheck public IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; + string ruleset = context.Beatmap.BeatmapInfo.Ruleset.ShortName; if (hasMoreThanOneDecimalPlace(diff.ApproachRate)) yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); @@ -38,7 +38,7 @@ public IEnumerable Run(BeatmapVerifierContext context) if (hasMoreThanOneDecimalPlace(diff.CircleSize)) yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); - if (isOutOfRange(diff.CircleSize)) + if (ruleset != "mania" && isOutOfRange(diff.CircleSize)) yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); From 2d6a3b8e2b5e3f0023957868a0706f5df162e110 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 20 Mar 2024 16:51:27 -0300 Subject: [PATCH 085/106] Remove warning for 10K+ --- .../Editor/Checks/CheckKeyCountTest.cs | 12 ------------ .../Edit/Checks/CheckKeyCount.cs | 16 ---------------- 2 files changed, 28 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs index 91361e2a625a..564c61154835 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs @@ -55,18 +55,6 @@ public void TestKeycountSmallerThanFour() Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountTooLow); } - [Test] - public void TestKeycountHigherThanTen() - { - beatmap.Difficulty.CircleSize = 11; - - var context = getContext(); - var issues = check.Run(context).ToList(); - - Assert.That(issues, Has.Count.EqualTo(1)); - Assert.That(issues.Single().Template is CheckKeyCount.IssueTemplateKeycountNonStandard); - } - private BeatmapVerifierContext getContext() { return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs index 57991bd3ffdf..51ead5f423fe 100644 --- a/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckKeyCount.cs @@ -14,7 +14,6 @@ public class CheckKeyCount : ICheck public IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateKeycountTooLow(this), - new IssueTemplateKeycountNonStandard(this), }; public IEnumerable Run(BeatmapVerifierContext context) @@ -25,11 +24,6 @@ public IEnumerable Run(BeatmapVerifierContext context) { yield return new IssueTemplateKeycountTooLow(this).Create(diff.CircleSize); } - - if (diff.CircleSize > 10) - { - yield return new IssueTemplateKeycountNonStandard(this).Create(diff.CircleSize); - } } public class IssueTemplateKeycountTooLow : IssueTemplate @@ -41,15 +35,5 @@ public IssueTemplateKeycountTooLow(ICheck check) public Issue Create(float current) => new Issue(this, current); } - - public class IssueTemplateKeycountNonStandard : IssueTemplate - { - public IssueTemplateKeycountNonStandard(ICheck check) - : base(check, IssueType.Warning, "Key count {0} is higher than 10.") - { - } - - public Issue Create(float current) => new Issue(this, current); - } } } From b132734f9c429da05b976544b266528c765019e6 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 20 Mar 2024 18:54:38 -0300 Subject: [PATCH 086/106] Fix codefactor issues --- .../Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs index 3c454a57fe11..db154f4efc0b 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs @@ -27,21 +27,18 @@ public IEnumerable Run(BeatmapVerifierContext context) if (isOutOfRange(diff.ApproachRate)) yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); - if (hasMoreThanOneDecimalPlace(diff.OverallDifficulty)) yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); if (isOutOfRange(diff.OverallDifficulty)) yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); - if (hasMoreThanOneDecimalPlace(diff.CircleSize)) yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); if (ruleset != "mania" && isOutOfRange(diff.CircleSize)) yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); - if (hasMoreThanOneDecimalPlace(diff.DrainRate)) yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); From a07d5115bfef749e2b766a554ceb420c82206dd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Mar 2024 11:42:22 +0800 Subject: [PATCH 087/106] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index de7497d58ef2..0e091dbd376e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 55d66d4615e65bdbcc794d385da3210aa130d7cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Mar 2024 11:45:46 +0800 Subject: [PATCH 088/106] Add sounds to countdown --- osu.Game/Screens/Play/DelayedResumeOverlay.cs | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/DelayedResumeOverlay.cs b/osu.Game/Screens/Play/DelayedResumeOverlay.cs index 8bb3ae8182a1..147d48ae0218 100644 --- a/osu.Game/Screens/Play/DelayedResumeOverlay.cs +++ b/osu.Game/Screens/Play/DelayedResumeOverlay.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -47,6 +49,8 @@ public partial class DelayedResumeOverlay : ResumeOverlay private SpriteText countdownText = null!; private CircularProgress countdownProgress = null!; + private Sample? sampleCountdown; + public DelayedResumeOverlay() { Anchor = Anchor.Centre; @@ -54,7 +58,7 @@ public DelayedResumeOverlay() } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { Add(outerContent = new Circle { @@ -103,6 +107,8 @@ private void load() } } }); + + sampleCountdown = audio.Samples.Get(@"Gameplay/resume-countdown"); } protected override void PopIn() @@ -164,13 +170,24 @@ private void updateCountdown() countdownProgress.Progress = amountTimePassed; countdownProgress.InnerRadius = progress_stroke_width / progress_size / countdownProgress.Scale.X; - if (countdownCount != newCount && newCount > 0) + if (countdownCount != newCount) { - countdownText.Text = Math.Max(1, newCount).ToString(); - countdownText.ScaleTo(0.25f).Then().ScaleTo(1, 200, Easing.OutQuint); - outerContent.Delay(25).Then().ScaleTo(1.05f, 100).Then().ScaleTo(1f, 200, Easing.Out); + if (newCount > 0) + { + countdownText.Text = Math.Max(1, newCount).ToString(); + countdownText.ScaleTo(0.25f).Then().ScaleTo(1, 200, Easing.OutQuint); + outerContent.Delay(25).Then().ScaleTo(1.05f, 100).Then().ScaleTo(1f, 200, Easing.Out); + + countdownBackground.FlashColour(colourProvider.Background3, 400, Easing.Out); + } + + var chan = sampleCountdown?.GetChannel(); - countdownBackground.FlashColour(colourProvider.Background3, 400, Easing.Out); + if (chan != null) + { + chan.Frequency.Value = newCount == 0 ? 0.5f : 1; + chan.Play(); + } } countdownCount = newCount; From b99b0337cffd33519b1323adbda98cb13b395f63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Mar 2024 13:06:25 +0800 Subject: [PATCH 089/106] Adjust text slightly --- osu.Game/Localisation/NotificationsStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 5328bcd066ed..3188ca55334a 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -126,9 +126,9 @@ public static class NotificationsStrings public static LocalisableString UpdateReadyToInstall => new TranslatableString(getKey(@"update_ready_to_install"), @"Update ready to install. Click to restart!"); /// - /// "This is not an official build of the game and scores will not be submitted." + /// "This is not an official build of the game. Scores will not be submitted and other online systems may not work as intended." /// - public static LocalisableString NotOfficialBuild => new TranslatableString(getKey(@"not_official_build"), @"This is not an official build of the game and scores will not be submitted."); + public static LocalisableString NotOfficialBuild => new TranslatableString(getKey(@"not_official_build"), @"This is not an official build of the game. Scores will not be submitted and other online systems may not work as intended."); /// /// "Downloading update..." From a4822fd615ef983be0399182f98159fc8546e427 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Fri, 22 Mar 2024 01:37:06 -0300 Subject: [PATCH 090/106] Make `CheckAbnormalDifficultySettings` abstract --- .../CheckAbnormalDifficultySettingsTest.cs | 93 ------------------- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 3 - .../Checks/CheckAbnormalDifficultySettings.cs | 41 ++------ 3 files changed, 8 insertions(+), 129 deletions(-) delete mode 100644 osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs b/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs deleted file mode 100644 index 94305755c443..000000000000 --- a/osu.Game.Tests/Editing/Checks/CheckAbnormalDifficultySettingsTest.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Edit.Checks; -using NUnit.Framework; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Edit; -using osu.Game.Tests.Beatmaps; -using System.Linq; - -namespace osu.Game.Tests.Editing.Checks -{ - [TestFixture] - public class CheckAbnormalDifficultySettingsTest - { - private CheckAbnormalDifficultySettings check = null!; - - private IBeatmap beatmap = new Beatmap(); - - [SetUp] - public void Setup() - { - check = new CheckAbnormalDifficultySettings(); - beatmap.Difficulty = new(); - } - - [Test] - public void TestSettingsNormal() - { - var context = getContext(); - var issues = check.Run(context).ToList(); - - Assert.That(issues, Has.Count.EqualTo(0)); - } - - [Test] - public void TestAllSettingsMoreThanOneDecimal() - { - beatmap.Difficulty = new() - { - ApproachRate = 5.55f, - OverallDifficulty = 7.7777f, - CircleSize = 4.444f, - DrainRate = 1.1111111111f, - }; - - var context = getContext(); - var issues = check.Run(context).ToList(); - - Assert.That(issues, Has.Count.EqualTo(4)); - } - - [Test] - public void TestAllSettingsLessThanZero() - { - beatmap.Difficulty = new() - { - ApproachRate = -1, - OverallDifficulty = -20, - CircleSize = -11, - DrainRate = -34, - }; - - var context = getContext(); - var issues = check.Run(context).ToList(); - - Assert.That(issues, Has.Count.EqualTo(4)); - } - - [Test] - public void TestAllSettingsHigherThanTen() - { - beatmap.Difficulty = new() - { - ApproachRate = 14, - OverallDifficulty = 24, - CircleSize = 30, - DrainRate = 90, - }; - - var context = getContext(); - var issues = check.Run(context).ToList(); - - Assert.That(issues, Has.Count.EqualTo(4)); - } - - private BeatmapVerifierContext getContext() - { - return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); - } - } -} diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index edabe941b7c8..2eb52e88c728 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -42,9 +42,6 @@ public class BeatmapVerifier : IBeatmapVerifier // Events new CheckBreaks(), - - // Settings - new CheckAbnormalDifficultySettings(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs index db154f4efc0b..93592a866be7 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs @@ -6,9 +6,9 @@ namespace osu.Game.Rulesets.Edit.Checks { - public class CheckAbnormalDifficultySettings : ICheck + public abstract class CheckAbnormalDifficultySettings : ICheck { - public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Abnormal difficulty settings"); + public abstract CheckMetadata Metadata { get; } public IEnumerable PossibleTemplates => new IssueTemplate[] { @@ -16,42 +16,17 @@ public class CheckAbnormalDifficultySettings : ICheck new IssueTemplateOutOfRange(this), }; - public IEnumerable Run(BeatmapVerifierContext context) - { - var diff = context.Beatmap.Difficulty; - string ruleset = context.Beatmap.BeatmapInfo.Ruleset.ShortName; - - if (hasMoreThanOneDecimalPlace(diff.ApproachRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); - - if (isOutOfRange(diff.ApproachRate)) - yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); - - if (hasMoreThanOneDecimalPlace(diff.OverallDifficulty)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); - - if (isOutOfRange(diff.OverallDifficulty)) - yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); - - if (hasMoreThanOneDecimalPlace(diff.CircleSize)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); - - if (ruleset != "mania" && isOutOfRange(diff.CircleSize)) - yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); - - if (hasMoreThanOneDecimalPlace(diff.DrainRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); - - if (isOutOfRange(diff.DrainRate)) - yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); - } + public abstract IEnumerable Run(BeatmapVerifierContext context); - private bool isOutOfRange(float setting) + /// + /// If the setting is out of the boundaries set by the editor (0 - 10) + /// + protected bool OutOfRange(float setting) { return setting < 0f || setting > 10f; } - private bool hasMoreThanOneDecimalPlace(float setting) + protected bool HasMoreThanOneDecimalPlace(float setting) { return float.Round(setting, 1) != setting; } From e2df0981843c3428fa11a232d341d97ac3e0505f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Mar 2024 08:25:03 +0100 Subject: [PATCH 091/106] Add failing test case for desired artist sort behaviour --- .../SongSelect/TestSceneBeatmapCarousel.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index de2ae3708faa..c0102b238c8e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -666,6 +666,56 @@ public void TestSorting() AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Artist == zzz_lowercase); } + [Test] + public void TestSortByArtistUsesTitleAsTiebreaker() + { + var sets = new List(); + + AddStep("Populuate beatmap sets", () => + { + sets.Clear(); + + for (int i = 0; i < 20; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(); + + if (i == 4) + { + set.Beatmaps.ForEach(b => + { + b.Metadata.Artist = "ZZZ"; + b.Metadata.Title = "AAA"; + }); + } + + if (i == 8) + { + set.Beatmaps.ForEach(b => + { + b.Metadata.Artist = "ZZZ"; + b.Metadata.Title = "ZZZ"; + }); + } + + sets.Add(set); + } + }); + + loadBeatmaps(sets); + + AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); + AddAssert("Check last item", () => + { + var lastItem = carousel.BeatmapSets.Last(); + return lastItem.Metadata.Artist == "ZZZ" && lastItem.Metadata.Title == "ZZZ"; + }); + AddAssert("Check second last item", () => + { + var secondLastItem = carousel.BeatmapSets.SkipLast(1).Last(); + return secondLastItem.Metadata.Artist == "ZZZ" && secondLastItem.Metadata.Title == "AAA"; + }); + } + /// /// Ensures stability is maintained on different sort modes for items with equal properties. /// From a1880e89c299f55a8907050988b8200e854ed34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Mar 2024 08:31:59 +0100 Subject: [PATCH 092/106] Use title as tiebreaker when sorting beatmap carousel by artist Closes https://github.com/ppy/osu/issues/27548. Reference: https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameplayElements/Beatmaps/BeatmapTreeManager.cs#L341-L347 --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 43c9c621e87a..7e1569980438 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -69,6 +69,8 @@ public override int CompareTo(FilterCriteria criteria, CarouselItem other) default: case SortMode.Artist: comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist); + if (comparison == 0) + goto case SortMode.Title; break; case SortMode.Title: From 6fa663c8cac732c6d705955f52565f1f78b7eddd Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Fri, 22 Mar 2024 14:48:22 -0300 Subject: [PATCH 093/106] Make check ruleset specific --- ...heckCatchAbnormalDifficultySettingsTest.cs | 158 ++++++++++++++ .../Edit/CatchBeatmapVerifier.cs | 3 +- .../CheckCatchAbnormalDifficultySettings.cs | 38 ++++ ...heckManiaAbnormalDifficultySettingsTest.cs | 121 +++++++++++ .../CheckManiaAbnormalDifficultySettings.cs | 32 +++ .../Edit/ManiaBeatmapVerifier.cs | 2 + .../CheckOsuAbnormalDifficultySettingsTest.cs | 194 ++++++++++++++++++ .../CheckOsuAbnormalDifficultySettings.cs | 43 ++++ .../Edit/OsuBeatmapVerifier.cs | 3 + ...heckTaikoAbnormalDifficultySettingsTest.cs | 84 ++++++++ .../CheckTaikoAbnormalDifficultySettings.cs | 26 +++ .../Edit/TaikoBeatmapVerifier.cs | 24 +++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 + 13 files changed, 729 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs create mode 100644 osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs create mode 100644 osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs new file mode 100644 index 000000000000..2ae2e20215f6 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs @@ -0,0 +1,158 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Edit.Checks; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks +{ + [TestFixture] + public class CheckCatchAbnormalDifficultySettingsTest + { + private CheckCatchAbnormalDifficultySettings check = null!; + + private IBeatmap beatmap = new Beatmap(); + + [SetUp] + public void Setup() + { + check = new CheckCatchAbnormalDifficultySettings(); + + beatmap.BeatmapInfo.Ruleset = new CatchRuleset().RulesetInfo; + beatmap.Difficulty = new BeatmapDifficulty() + { + ApproachRate = 5, + CircleSize = 5, + DrainRate = 5, + }; + } + + [Test] + public void TestNormalSettings() + { + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestApproachRateTwoDecimals() + { + beatmap.Difficulty.ApproachRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestCircleSizeTwoDecimals() + { + beatmap.Difficulty.CircleSize = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestDrainRateTwoDecimals() + { + beatmap.Difficulty.DrainRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestApproachRateUnder() + { + beatmap.Difficulty.ApproachRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestCircleSizeUnder() + { + beatmap.Difficulty.CircleSize = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateUnder() + { + beatmap.Difficulty.DrainRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestApproachRateOver() + { + beatmap.Difficulty.ApproachRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestCircleSizeOver() + { + beatmap.Difficulty.CircleSize = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateOver() + { + beatmap.Difficulty.DrainRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs index c7a41a4e22f7..71da6d5014ae 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBeatmapVerifier.cs @@ -13,7 +13,8 @@ public class CatchBeatmapVerifier : IBeatmapVerifier { private readonly List checks = new List { - new CheckBananaShowerGap() + new CheckBananaShowerGap(), + new CheckCatchAbnormalDifficultySettings(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs new file mode 100644 index 000000000000..8295795f00f6 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Catch.Edit.Checks +{ + public class CheckCatchAbnormalDifficultySettings : CheckAbnormalDifficultySettings + { + public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks catch relevant settings"); + + public override IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (HasMoreThanOneDecimalPlace(diff.ApproachRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); + + if (OutOfRange(diff.ApproachRate)) + yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); + + if (HasMoreThanOneDecimalPlace(diff.CircleSize)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); + + if (OutOfRange(diff.CircleSize)) + yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); + + if (HasMoreThanOneDecimalPlace(diff.DrainRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + + if (OutOfRange(diff.DrainRate)) + yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs new file mode 100644 index 000000000000..6c585aace34d --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs @@ -0,0 +1,121 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Edit.Checks; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks +{ + [TestFixture] + public class CheckManiaAbnormalDifficultySettingsTest + { + private CheckManiaAbnormalDifficultySettings check = null!; + + private IBeatmap beatmap = new Beatmap(); + + [SetUp] + public void Setup() + { + check = new CheckManiaAbnormalDifficultySettings(); + + beatmap.BeatmapInfo.Ruleset = new ManiaRuleset().RulesetInfo; + beatmap.Difficulty = new BeatmapDifficulty() + { + OverallDifficulty = 5, + DrainRate = 5, + }; + } + + [Test] + public void TestNormalSettings() + { + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestOverallDifficultyTwoDecimals() + { + beatmap.Difficulty.OverallDifficulty = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestDrainRateTwoDecimals() + { + beatmap.Difficulty.DrainRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestOverallDifficultyUnder() + { + beatmap.Difficulty.OverallDifficulty = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateUnder() + { + beatmap.Difficulty.DrainRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestOverallDifficultyOver() + { + beatmap.Difficulty.OverallDifficulty = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateOver() + { + beatmap.Difficulty.DrainRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs new file mode 100644 index 000000000000..ae0cc3aa4c0a --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Mania.Edit.Checks +{ + public class CheckManiaAbnormalDifficultySettings : CheckAbnormalDifficultySettings + { + public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks mania relevant settings"); + + public override IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (OutOfRange(diff.OverallDifficulty)) + yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (HasMoreThanOneDecimalPlace(diff.DrainRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + + if (OutOfRange(diff.DrainRate)) + yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs index 778dcfc6e12f..4adabfa4d70a 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatmapVerifier.cs @@ -13,7 +13,9 @@ public class ManiaBeatmapVerifier : IBeatmapVerifier { private readonly List checks = new List { + // Settings new CheckKeyCount(), + new CheckManiaAbnormalDifficultySettings(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs new file mode 100644 index 000000000000..53ccd3c7a729 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs @@ -0,0 +1,194 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Osu.Edit.Checks; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit; +using osu.Game.Tests.Beatmaps; +using System.Linq; + +namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks +{ + [TestFixture] + public class CheckOsuAbnormalDifficultySettingsTest + { + private CheckOsuAbnormalDifficultySettings check = null!; + + private IBeatmap beatmap = new Beatmap(); + + [SetUp] + public void Setup() + { + check = new CheckOsuAbnormalDifficultySettings(); + + beatmap.Difficulty = new BeatmapDifficulty() + { + ApproachRate = 5, + CircleSize = 5, + DrainRate = 5, + OverallDifficulty = 5, + }; + } + + [Test] + public void TestNormalSettings() + { + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestApproachRateTwoDecimals() + { + beatmap.Difficulty.ApproachRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestCircleSizeTwoDecimals() + { + beatmap.Difficulty.CircleSize = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestDrainRateTwoDecimals() + { + beatmap.Difficulty.DrainRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestOverallDifficultyTwoDecimals() + { + beatmap.Difficulty.OverallDifficulty = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestApproachRateUnder() + { + beatmap.Difficulty.ApproachRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestCircleSizeUnder() + { + beatmap.Difficulty.CircleSize = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateUnder() + { + beatmap.Difficulty.DrainRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestOverallDifficultyUnder() + { + beatmap.Difficulty.OverallDifficulty = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestApproachRateOver() + { + beatmap.Difficulty.ApproachRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestCircleSizeOver() + { + beatmap.Difficulty.CircleSize = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateOver() + { + beatmap.Difficulty.DrainRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestOverallDifficultyOver() + { + beatmap.Difficulty.OverallDifficulty = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs new file mode 100644 index 000000000000..c1eca7fff7f0 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Osu.Edit.Checks +{ + public class CheckOsuAbnormalDifficultySettings : CheckAbnormalDifficultySettings + { + public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks osu relevant settings"); + public override IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (HasMoreThanOneDecimalPlace(diff.ApproachRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); + + if (OutOfRange(diff.ApproachRate)) + yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); + + if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (OutOfRange(diff.OverallDifficulty)) + yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (HasMoreThanOneDecimalPlace(diff.CircleSize)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); + + if (OutOfRange(diff.CircleSize)) + yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); + + if (HasMoreThanOneDecimalPlace(diff.DrainRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + + if (OutOfRange(diff.DrainRate)) + yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs index 325e9ed4cb92..4b01a1fc3998 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs @@ -21,6 +21,9 @@ public class OsuBeatmapVerifier : IBeatmapVerifier new CheckTimeDistanceEquality(), new CheckLowDiffOverlaps(), new CheckTooShortSliders(), + + // Settings + new CheckOsuAbnormalDifficultySettings(), }; public IEnumerable Run(BeatmapVerifierContext context) diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs new file mode 100644 index 000000000000..f10e62f3bff7 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Taiko.Edit.Checks; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit; +using osu.Game.Tests.Beatmaps; +using System.Linq; + +namespace osu.Game.Rulesets.Taiko.Tests.Editor.Checks +{ + [TestFixture] + public class CheckTaikoAbnormalDifficultySettingsTest + { + private CheckTaikoAbnormalDifficultySettings check = null!; + + private IBeatmap beatmap = new Beatmap(); + + [SetUp] + public void Setup() + { + check = new CheckTaikoAbnormalDifficultySettings(); + + beatmap.BeatmapInfo.Ruleset = new TaikoRuleset().RulesetInfo; + beatmap.Difficulty = new BeatmapDifficulty() + { + OverallDifficulty = 5, + }; + } + + [Test] + public void TestNormalSettings() + { + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestOverallDifficultyTwoDecimals() + { + beatmap.Difficulty.OverallDifficulty = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + + [Test] + public void TestOverallDifficultyUnder() + { + beatmap.Difficulty.OverallDifficulty = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestOverallDifficultyOver() + { + beatmap.Difficulty.OverallDifficulty = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + private BeatmapVerifierContext getContext() + { + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs new file mode 100644 index 000000000000..ce35f218536a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Taiko.Edit.Checks +{ + public class CheckTaikoAbnormalDifficultySettings : CheckAbnormalDifficultySettings + { + public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks taiko relevant settings"); + + public override IEnumerable Run(BeatmapVerifierContext context) + { + var diff = context.Beatmap.Difficulty; + + if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (OutOfRange(diff.OverallDifficulty)) + yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs new file mode 100644 index 000000000000..f5c3f1846dce --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBeatmapVerifier.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Taiko.Edit.Checks; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public class TaikoBeatmapVerifier : IBeatmapVerifier + { + private readonly List checks = new List + { + new CheckTaikoAbnormalDifficultySettings(), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + return checks.SelectMany(check => check.Run(context)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 24b0ec5d57b1..d7184bce60e6 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -188,6 +188,8 @@ public override IEnumerable GetModsFor(ModType type) public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this); + public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier(); + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap); public override PerformanceCalculator CreatePerformanceCalculator() => new TaikoPerformanceCalculator(); From 6fbe1a5b8d1a80c6a5b1e277ee015162b7ea8083 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 23 Mar 2024 19:22:47 -0300 Subject: [PATCH 094/106] Add video resolution check --- .../Checks/CheckVideoResolutionTest.cs | 86 +++++++++++++ .../Videos/test-video-resolution-high.mp4 | Bin 0 -> 13655 bytes osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 1 + .../Edit/Checks/CheckVideoResolution.cs | 117 ++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs create mode 100644 osu.Game.Tests/Resources/Videos/test-video-resolution-high.mp4 create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs b/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs new file mode 100644 index 000000000000..ab677a15d4c3 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs @@ -0,0 +1,86 @@ +using System.IO; +using System.Linq; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Storyboards; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckVideoResolutionTest + { + private CheckVideoResolution check = null!; + + private IBeatmap beatmap = null!; + + [SetUp] + public void Setup() + { + check = new CheckVideoResolution(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = + { + CheckTestHelpers.CreateMockFile("mp4"), + } + } + } + }; + } + + [Test] + public void TestNoVideo() + { + beatmap.BeatmapInfo.BeatmapSet?.Files.Clear(); + + var issues = check.Run(getContext(null)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(0)); + } + + [Test] + public void TestVideoAcceptableResolution() + { + using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + Assert.That(issues, Has.Count.EqualTo(0)); + } + } + + [Test] + public void TestVideoHighResolution() + { + using (var resourceStream = TestResources.OpenResource("Videos/test-video-resolution-high.mp4")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckVideoResolution.IssueTemplateHighResolution); + } + } + + private BeatmapVerifierContext getContext(Stream? resourceStream) + { + var storyboard = new Storyboard(); + var layer = storyboard.GetLayer("Video"); + layer.Add(new StoryboardVideo("abc123.mp4", 0)); + + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + mockWorkingBeatmap.As().SetupGet(w => w.Storyboard).Returns(storyboard); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Resources/Videos/test-video-resolution-high.mp4 b/osu.Game.Tests/Resources/Videos/test-video-resolution-high.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..fbdb00d3ad464a7f2d9d223d5fa9edb67fdaa689 GIT binary patch literal 13655 zcmeHNeQX>@6@PczVL*m4tPP}$L-S}hg&bg*ePO=GZEeMbb5vo#p-P>LJ4)5Ns zch`~B8a%$qmwy`2jYNdw-vopmjfC`>E|#gtJsYT8{RYT1XfIi(P|$oB0*sq zpGtgdCop3W(sEt%T;z?q;d(ye&y`r0=D_UPkB{GRO@&2S21 z&}dfEgEGsv;N9w1a+;yDHdS9Tv$+8j7L1zyp!~F#Yx%lH&CyvKmRS9q5yz17<*GYe zFKwWF+V$LELs*+*Y}1gl#EwF5+qsKNF8ok7uW)6Fy!Ny38Jjq{as2fL>sSr>ss76H3uE$U3*>WF{yW;; zS7^sF$iI3!awdDWcf;49u{YO+Uj+8=_C5X|^ksaRZv88BzT$eSg+46Z3hUPkE&d4T z1?bT2pq~)cuLpgPXkjPl+eC|&f%Zdfmx3N8TKrznZWtB?^fJ+sEudc#E!_+{3U&Pe z=xM0)I?#8Cn&Y4+iCR{H9wSK@P;qSbeTP7$?k0{x8W<{l8{ zvt~W$5K$ZE({_NU^M24Ik&Ly=uMkBupmRjAH0U%@9P^2vAnLjU^gL0`iECI}{{Z+_ zo=GI1LQF|~ttkM0=7S}mr&MI6x3achDOuZ^zPGlvPft+Du4rvtYb3|B`WW7|lC`bA zT(GuvZr<7kZT|^NTm6HArM-WerOm-LVsrgeT6UP~7=o#?l+ay`DLq9dCMH(koK+!T z)Kkx(FgroBZ>$DVJ@26{=copy5EFID92VLDn{E#8&Rf55hC9wNv|}>~h>gXFy))Yt z;ME*n!{N0Y&TK}|el~~Sz~OT^d@hH-hr@5=a3+~xKJz#nN2Q1}+Zym?VhY+XmBZI?cpHbe zb2!{uaU2});yh&zk8(KEgFtRw9KM#r6C8dEhr=8d$A{@7rYn$3=yTKD0rZd9J}~{6 zAu%$T=9cDT-{!;gFM#P^0MlO&-KdA@UkF#Ua38h@`?Kg#m=2tghTU*SIIoN0!W%yc z`W4*KX3$Z%BeAh$M+~HoECS8%R*Y8ZoV8d3r?i6y$fAx}T#+ z&3t$cC*vr#%|^2*}0oF;OLOU>ZK-H zVOmerR8LKP=XZU7I(+_g|BoJS`;ByZ*`>dX#S&doyQEr56N-{R zMSlkxbbfeq-|g*PQty_HXsPKcDmGg7xS(;Fu?DN7h zlw61940bXu9U0Fw(8jX8td(=SB~?i)YBDO}6CxQ+NSdBDEOjUu?UuV`Ny#b3xT`1S zv3R#U7LCUxQ%{bV8Oa?`lrBn7e%2^GINT(D{u|2<(&MGcr zl67>fD=ux-vx6Bg4Q7_D=Td`~joLhDqr;FsjcsjzFMW01iHl#4H$78-=&7z- z4xjYDkRrje_QmgaED5ki<63Bk!l=n^X-e2CzC5w+{s z<0>4<%Q!UP3&&p^b*3;Qfwx5h!@V%cR_`OT+jo9+>Wx=Ks94RF--nr3P~jEgQFw(I zyauYL9p1w7Qi3SIZ& z%Mf(K7%uP7rKTTw9q2M{o*u$IJTOVZS6C0xX!(fObUFh3+6;^T0SSbM0*w|PoaGr8 z*^7+GZM?OX0CtN#FS<<~4c6f5rYbqvVZ z(nW@16+U&ug<=(SORYVicoAHevAaoT#n7$^<57MaF=7Y|I1}+YI4_; zWvcsMx&r+1wrR28bqL9r)&9TvrLxR$iVManI16#X*fgu{@3ydztX$xtgRx>%1PvD# nl9krXg0TnQ0l!rC9a$sfC&QIzd5O0gqJFh2Q*qv;=F9&8=0zb8 literal 0 HcmV?d00001 diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 4a316afd2226..95f79391efcf 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -18,6 +18,7 @@ public class BeatmapVerifier : IBeatmapVerifier // Resources new CheckBackgroundPresence(), new CheckBackgroundQuality(), + new CheckVideoResolution(), // Audio new CheckAudioPresence(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs new file mode 100644 index 000000000000..831962e2e9bd --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs @@ -0,0 +1,117 @@ +// 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.IO; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.IO.FileAbstraction; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Storyboards; +using TagLib; +using File = TagLib.File; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckVideoResolution : ICheck + { + private const int max_video_width = 1280; + + private const int max_video_height = 720; + + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Resources, "Too high video resolution."); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateHighResolution(this), + new IssueTemplateFileError(this), + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + var videoPaths = getVideoPaths(context.WorkingBeatmap.Storyboard); + + foreach (string filename in videoPaths) + { + string? storagePath = beatmapSet?.GetPathForFile(filename); + + // Don't report any issues for missing video here since another check is already doing that (CheckAudioInVideo) + if (storagePath == null) continue; + + Issue issue; + + try + { + using (Stream data = context.WorkingBeatmap.GetStream(storagePath)) + using (File tagFile = File.Create(new StreamFileAbstraction(filename, data))) + { + int height = tagFile.Properties.VideoHeight; + int width = tagFile.Properties.VideoWidth; + + if (height <= max_video_height || width <= max_video_width) + continue; + + issue = new IssueTemplateHighResolution(this).Create(filename, width, height); + } + } + catch (CorruptFileException) + { + issue = new IssueTemplateFileError(this).Create(filename, "Corrupt file"); + } + catch (UnsupportedFormatException) + { + issue = new IssueTemplateFileError(this).Create(filename, "Unsupported format"); + } + catch (Exception ex) + { + issue = new IssueTemplateFileError(this).Create(filename, "Internal failure - see logs for more info"); + Logger.Log($"Failed when running {nameof(CheckVideoResolution)}: {ex}"); + } + + yield return issue; + } + } + + private List getVideoPaths(Storyboard storyboard) + { + var videoPaths = new List(); + + foreach (var layer in storyboard.Layers) + { + foreach (var element in layer.Elements) + { + if (element is not StoryboardVideo video) + continue; + + if (!videoPaths.Contains(video.Path)) + videoPaths.Add(video.Path); + } + } + + return videoPaths; + } + + public class IssueTemplateHighResolution : IssueTemplate + { + public IssueTemplateHighResolution(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" resolution exceeds 1280x720 ({1}x{2})") + { + } + + public Issue Create(string filename, int width, int height) => new Issue(this, filename, width, height); + } + + public class IssueTemplateFileError : IssueTemplate + { + public IssueTemplateFileError(ICheck check) + : base(check, IssueType.Error, "Could not check resolution for \"{0}\" ({1}).") + { + } + + public Issue Create(string filename, string errorReason) => new Issue(this, filename, errorReason); + } + + } +} From eb938272040be14caa292fe65800356a6ae03a1a Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 23 Mar 2024 23:11:13 -0300 Subject: [PATCH 095/106] Add missing copyright header --- osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs b/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs index ab677a15d4c3..1e16c67aabc6 100644 --- a/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckVideoResolutionTest.cs @@ -1,3 +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 System.IO; using System.Linq; using Moq; From 8a05fecad5862d744d49b8febcb389e4e22b9850 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Sat, 23 Mar 2024 23:28:55 -0300 Subject: [PATCH 096/106] Fix formatting issue --- osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs index 831962e2e9bd..1b603b7e4744 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckVideoResolution.cs @@ -112,6 +112,5 @@ public IssueTemplateFileError(ICheck check) public Issue Create(string filename, string errorReason) => new Issue(this, filename, errorReason); } - } } From 3db88fbcea288f5c4136739bd26e0bdab9e0b05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 17:54:20 +0100 Subject: [PATCH 097/106] Use less confusing message format when logging discord errors The "code" is a number, so it looked weird when put in the middle without any nearby punctuation. Example: An error occurred with Discord RPC Client: 5005 secrets cannot currently be sent with buttons --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index d78459ff28d4..eaa9a90f27ab 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -75,7 +75,7 @@ private void load(OsuConfigManager config) }; client.OnReady += onReady; - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error); + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error); // A URI scheme is required to support game invitations, as well as informing Discord of the game executable path to support launching the game when a user clicks on join/spectate. client.RegisterUriScheme(); From e95f29cf4ba8840e0305bbae7637d01ddc03f285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 17:57:13 +0100 Subject: [PATCH 098/106] Rename `updatePresence() => schedulePresenceUpdate()` The method doesn't actually update anything by itself, and I want to free up the `updatePresence()` name for the actual update. --- osu.Desktop/DiscordRichPresence.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index eaa9a90f27ab..811f2f354858 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -94,10 +94,10 @@ private void load(OsuConfigManager config) activity.BindTo(u.NewValue.Activity); }, true); - ruleset.BindValueChanged(_ => updatePresence()); - status.BindValueChanged(_ => updatePresence()); - activity.BindValueChanged(_ => updatePresence()); - privacyMode.BindValueChanged(_ => updatePresence()); + ruleset.BindValueChanged(_ => schedulePresenceUpdate()); + status.BindValueChanged(_ => schedulePresenceUpdate()); + activity.BindValueChanged(_ => schedulePresenceUpdate()); + privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); multiplayerClient.RoomUpdated += onRoomUpdated; client.Initialize(); @@ -111,14 +111,14 @@ private void onReady(object _, ReadyMessage __) if (client.CurrentPresence != null) client.SetPresence(null); - updatePresence(); + schedulePresenceUpdate(); } - private void onRoomUpdated() => updatePresence(); + private void onRoomUpdated() => schedulePresenceUpdate(); private ScheduledDelegate? presenceUpdateDelegate; - private void updatePresence() + private void schedulePresenceUpdate() { presenceUpdateDelegate?.Cancel(); presenceUpdateDelegate = Scheduler.AddDelayed(() => From a398754a27ac26a1f6e57790b2c609226ad805fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 18:00:42 +0100 Subject: [PATCH 099/106] Merge all presence methods into one I'm about to make them interdependent (and it's discord's fault), so it doesn't really make sense to make them separate at this point I don't think. And it felt weird anyway. --- osu.Desktop/DiscordRichPresence.cs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 811f2f354858..d67437ba6236 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -134,16 +134,14 @@ private void schedulePresenceUpdate() bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; - updatePresenceStatus(hideIdentifiableInformation); - updatePresenceParty(hideIdentifiableInformation); - updatePresenceAssets(); - + updatePresence(hideIdentifiableInformation); client.SetPresence(presence); }, 200); } - private void updatePresenceStatus(bool hideIdentifiableInformation) + private void updatePresence(bool hideIdentifiableInformation) { + // user activity if (activity.Value != null) { presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); @@ -170,10 +168,8 @@ private void updatePresenceStatus(bool hideIdentifiableInformation) presence.State = "Idle"; presence.Details = string.Empty; } - } - private void updatePresenceParty(bool hideIdentifiableInformation) - { + // user party if (!hideIdentifiableInformation && multiplayerClient.Room != null) { MultiplayerRoom room = multiplayerClient.Room; @@ -201,11 +197,9 @@ private void updatePresenceParty(bool hideIdentifiableInformation) presence.Party = null; presence.Secrets.JoinSecret = null; } - } - private void updatePresenceAssets() - { - // update user information + // game images: + // large image tooltip if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; else @@ -216,7 +210,7 @@ private void updatePresenceAssets() presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); } - // update ruleset + // small image presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; presence.Assets.SmallImageText = ruleset.Value.Name; } From 53c3aec3c364a2541180e1bc34b4c40ee688f2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 18:02:54 +0100 Subject: [PATCH 100/106] Fix discord RPC errors in multiplayer Reproduction steps: 1. Go to multiplayer 2. Create a room 3. Play a map to completion 4. Wait for "secrets cannot currently be sent with buttons" error messages The fix is to clear the buttons since they're the less important ones. --- osu.Desktop/DiscordRichPresence.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index d67437ba6236..6e8554d617d3 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -191,6 +191,9 @@ private void updatePresence(bool hideIdentifiableInformation) }; presence.Secrets.JoinSecret = JsonConvert.SerializeObject(roomSecret); + // discord cannot handle both secrets and buttons at the same time, so we need to choose something. + // the multiplayer room seems more important. + presence.Buttons = null; } else { From 6266af8a5696edb32da2f57b877f84dbcab66e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 18:57:58 +0100 Subject: [PATCH 101/106] Fix taiko legacy score simulator not including swell tick score gain into bonus portion Reported in https://discord.com/channels/188630481301012481/1097318920991559880/1221836384038551613. Example score: https://osu.ppy.sh/scores/1855965185 The cause of the overestimation was an error in taiko's score simulator. In lazer taiko, swell ticks don't give any score anymore, while they did in stable. For all intents and purposes, swell ticks can be considered "bonus" objects that "don't give any actual bonus score". Which is to say, during simulation of a legacy score swell ticks hit should be treated as bonus, because if they aren't, then otherwise they will be treated essentially as *normal hits*, meaning that they will be included in the *accuracy* portion of score, which breaks all sorts of follow-up assumptions: - The accuracy portion of the best possible total score becomes overinflated in comparison to reality, while the combo portion of that maximum score becomes underestimated. - Because the affected score has low accuracy, the estimated accuracy portion of the score (as given by maximmum accuracy portion of score times the actual numerical accuracy of the score) is also low. - However, the next step is estimating the combo portion, which is done by taking legacy total score, subtracting the aforementioned estimation for accuracy total score from that, and then dividing the result by the maximum achievable combo score on the map. Because most of actual "combo" score from swell ticks was "moved" into the accuracy portion due to the aforementioned error, the maximum achievable combo score becomes so small that the estimated combo portion exceeds 1. Instead, this change makes it so that gains from swell ticks are treated as "bonus", which means that they are excluded from the accuracy portion of score and instead count into the bonus portion of score, bringing the scores concerned more in line with expectations - although due to pessimistic assumptions in the simulation of the swell itself, the conversion will still overestimate total score for affected scores, just not by *that* much. --- osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs index 66ff0fc3d99c..9839d942776e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs @@ -95,6 +95,8 @@ private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attribut case SwellTick: scoreIncrease = 300; increaseCombo = false; + isBonus = true; + bonusResult = HitResult.IgnoreHit; break; case DrumRollTick: From 73926592b91e1cf6348b3e7ae3c68142dbbd8d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Mar 2024 19:27:38 +0100 Subject: [PATCH 102/106] Bump legacy score version to recalculate all scores --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 93f51ee74dda..0f00cce080fa 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -46,9 +46,10 @@ public class LegacyScoreEncoder /// 30000013: All local scores will use lazer definitions of ranks for consistency. Recalculates the rank of all scores. /// 30000014: Fix edge cases in conversion for osu! scores on selected beatmaps. Reconvert all scores. /// 30000015: Fix osu! standardised score estimation algorithm violating basic invariants. Reconvert all scores. + /// 30000016: Fix taiko standardised score estimation algorithm not including swell tick score gain into bonus portion. Reconvert all scores. /// /// - public const int LATEST_VERSION = 30000015; + public const int LATEST_VERSION = 30000016; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. From 9b923b19094bbcdf3f130d005085de70368cb8c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 10:55:49 +0100 Subject: [PATCH 103/106] Fix code quality issues --- .../Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs | 4 ++-- .../Editor/Checks/CheckKeyCountTest.cs | 2 +- .../Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs | 4 ++-- .../Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs | 4 ++-- .../Edit/Checks/CheckOsuAbnormalDifficultySettings.cs | 1 + .../Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs | 4 ++-- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs index 2ae2e20215f6..33aa4cba5db7 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/Checks/CheckCatchAbnormalDifficultySettingsTest.cs @@ -17,7 +17,7 @@ public class CheckCatchAbnormalDifficultySettingsTest { private CheckCatchAbnormalDifficultySettings check = null!; - private IBeatmap beatmap = new Beatmap(); + private readonly IBeatmap beatmap = new Beatmap(); [SetUp] public void Setup() @@ -25,7 +25,7 @@ public void Setup() check = new CheckCatchAbnormalDifficultySettings(); beatmap.BeatmapInfo.Ruleset = new CatchRuleset().RulesetInfo; - beatmap.Difficulty = new BeatmapDifficulty() + beatmap.Difficulty = new BeatmapDifficulty { ApproachRate = 5, CircleSize = 5, diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs index 564c61154835..b40a62176c1d 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckKeyCountTest.cs @@ -23,7 +23,7 @@ public void Setup() { check = new CheckKeyCount(); - beatmap = new Beatmap() + beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs index 6c585aace34d..da5ab037e5b6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/Checks/CheckManiaAbnormalDifficultySettingsTest.cs @@ -17,7 +17,7 @@ public class CheckManiaAbnormalDifficultySettingsTest { private CheckManiaAbnormalDifficultySettings check = null!; - private IBeatmap beatmap = new Beatmap(); + private readonly IBeatmap beatmap = new Beatmap(); [SetUp] public void Setup() @@ -25,7 +25,7 @@ public void Setup() check = new CheckManiaAbnormalDifficultySettings(); beatmap.BeatmapInfo.Ruleset = new ManiaRuleset().RulesetInfo; - beatmap.Difficulty = new BeatmapDifficulty() + beatmap.Difficulty = new BeatmapDifficulty { OverallDifficulty = 5, DrainRate = 5, diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs index 53ccd3c7a729..5f49714d9328 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOsuAbnormalDifficultySettingsTest.cs @@ -17,14 +17,14 @@ public class CheckOsuAbnormalDifficultySettingsTest { private CheckOsuAbnormalDifficultySettings check = null!; - private IBeatmap beatmap = new Beatmap(); + private readonly IBeatmap beatmap = new Beatmap(); [SetUp] public void Setup() { check = new CheckOsuAbnormalDifficultySettings(); - beatmap.Difficulty = new BeatmapDifficulty() + beatmap.Difficulty = new BeatmapDifficulty { ApproachRate = 5, CircleSize = 5, diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs index c1eca7fff7f0..7ad861f317fc 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks public class CheckOsuAbnormalDifficultySettings : CheckAbnormalDifficultySettings { public override CheckMetadata Metadata => new CheckMetadata(CheckCategory.Settings, "Checks osu relevant settings"); + public override IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs index f10e62f3bff7..4a6cf0313a05 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs @@ -17,7 +17,7 @@ public class CheckTaikoAbnormalDifficultySettingsTest { private CheckTaikoAbnormalDifficultySettings check = null!; - private IBeatmap beatmap = new Beatmap(); + private readonly IBeatmap beatmap = new Beatmap(); [SetUp] public void Setup() @@ -25,7 +25,7 @@ public void Setup() check = new CheckTaikoAbnormalDifficultySettings(); beatmap.BeatmapInfo.Ruleset = new TaikoRuleset().RulesetInfo; - beatmap.Difficulty = new BeatmapDifficulty() + beatmap.Difficulty = new BeatmapDifficulty { OverallDifficulty = 5, }; From 8fb308c1925e1cdbda3eba6e58b7ef5c8fb1fe89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 10:57:20 +0100 Subject: [PATCH 104/106] Add failing test coverage for checking taiko HP too I was wrong, taiko uses HP (to calculate miss penalty). --- ...heckTaikoAbnormalDifficultySettingsTest.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs index 4a6cf0313a05..6a50fd095627 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/Checks/CheckTaikoAbnormalDifficultySettingsTest.cs @@ -52,6 +52,18 @@ public void TestOverallDifficultyTwoDecimals() Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); } + [Test] + public void TestDrainRateTwoDecimals() + { + beatmap.Difficulty.DrainRate = 5.55f; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateMoreThanOneDecimal); + } + [Test] public void TestOverallDifficultyUnder() { @@ -76,6 +88,30 @@ public void TestOverallDifficultyOver() Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); } + [Test] + public void TestDrainRateUnder() + { + beatmap.Difficulty.DrainRate = -10; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + + [Test] + public void TestDrainRateOver() + { + beatmap.Difficulty.DrainRate = 20; + + var context = getContext(); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAbnormalDifficultySettings.IssueTemplateOutOfRange); + } + private BeatmapVerifierContext getContext() { return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); From e7cf1ab4df36879306ef6e4c38b535ba370a0e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 10:58:39 +0100 Subject: [PATCH 105/106] Add checks for taiko drain rate --- .../Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs index ce35f218536a..10e2867ca04d 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs @@ -21,6 +21,12 @@ public override IEnumerable Run(BeatmapVerifierContext context) if (OutOfRange(diff.OverallDifficulty)) yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + + if (HasMoreThanOneDecimalPlace(diff.DrainRate)) + yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + + if (OutOfRange(diff.DrainRate)) + yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); } } } From 1866b4b6b10d1990db311eb96ae02c5c5609e918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Mar 2024 11:13:03 +0100 Subject: [PATCH 106/106] Refactor abstract check to reduce duplication --- .../CheckCatchAbnormalDifficultySettings.cs | 25 +++++++------- .../CheckManiaAbnormalDifficultySettings.cs | 17 +++++----- .../CheckOsuAbnormalDifficultySettings.cs | 33 ++++++++++--------- .../CheckTaikoAbnormalDifficultySettings.cs | 17 +++++----- .../Checks/CheckAbnormalDifficultySettings.cs | 13 +++++--- 5 files changed, 57 insertions(+), 48 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs index 8295795f00f6..d2c3df087262 100644 --- a/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Catch/Edit/Checks/CheckCatchAbnormalDifficultySettings.cs @@ -15,24 +15,25 @@ public class CheckCatchAbnormalDifficultySettings : CheckAbnormalDifficultySetti public override IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; + Issue? issue; - if (HasMoreThanOneDecimalPlace(diff.ApproachRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); + if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue)) + yield return issue; - if (OutOfRange(diff.ApproachRate)) - yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); + if (OutOfRange("Approach rate", diff.ApproachRate, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.CircleSize)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); + if (HasMoreThanOneDecimalPlace("Circle size", diff.CircleSize, out issue)) + yield return issue; - if (OutOfRange(diff.CircleSize)) - yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); + if (OutOfRange("Circle size", diff.CircleSize, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.DrainRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue)) + yield return issue; - if (OutOfRange(diff.DrainRate)) - yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + if (OutOfRange("Drain rate", diff.DrainRate, out issue)) + yield return issue; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs index ae0cc3aa4c0a..233c602c215b 100644 --- a/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Mania/Edit/Checks/CheckManiaAbnormalDifficultySettings.cs @@ -15,18 +15,19 @@ public class CheckManiaAbnormalDifficultySettings : CheckAbnormalDifficultySetti public override IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; + Issue? issue; - if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (OutOfRange(diff.OverallDifficulty)) - yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + if (OutOfRange("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.DrainRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue)) + yield return issue; - if (OutOfRange(diff.DrainRate)) - yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + if (OutOfRange("Drain rate", diff.DrainRate, out issue)) + yield return issue; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs index 7ad861f317fc..1c44d546335e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOsuAbnormalDifficultySettings.cs @@ -15,30 +15,31 @@ public class CheckOsuAbnormalDifficultySettings : CheckAbnormalDifficultySetting public override IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; + Issue? issue; - if (HasMoreThanOneDecimalPlace(diff.ApproachRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Approach rate", diff.ApproachRate); + if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue)) + yield return issue; - if (OutOfRange(diff.ApproachRate)) - yield return new IssueTemplateOutOfRange(this).Create("Approach rate", diff.ApproachRate); + if (OutOfRange("Approach rate", diff.ApproachRate, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (OutOfRange(diff.OverallDifficulty)) - yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + if (OutOfRange("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.CircleSize)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Circle size", diff.CircleSize); + if (HasMoreThanOneDecimalPlace("Circle size", diff.CircleSize, out issue)) + yield return issue; - if (OutOfRange(diff.CircleSize)) - yield return new IssueTemplateOutOfRange(this).Create("Circle size", diff.CircleSize); + if (OutOfRange("Circle size", diff.CircleSize, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.DrainRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue)) + yield return issue; - if (OutOfRange(diff.DrainRate)) - yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + if (OutOfRange("Drain rate", diff.DrainRate, out issue)) + yield return issue; } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs index 10e2867ca04d..38ba7b1b0171 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Checks/CheckTaikoAbnormalDifficultySettings.cs @@ -15,18 +15,19 @@ public class CheckTaikoAbnormalDifficultySettings : CheckAbnormalDifficultySetti public override IEnumerable Run(BeatmapVerifierContext context) { var diff = context.Beatmap.Difficulty; + Issue? issue; - if (HasMoreThanOneDecimalPlace(diff.OverallDifficulty)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Overall difficulty", diff.OverallDifficulty); + if (HasMoreThanOneDecimalPlace("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (OutOfRange(diff.OverallDifficulty)) - yield return new IssueTemplateOutOfRange(this).Create("Overall difficulty", diff.OverallDifficulty); + if (OutOfRange("Overall difficulty", diff.OverallDifficulty, out issue)) + yield return issue; - if (HasMoreThanOneDecimalPlace(diff.DrainRate)) - yield return new IssueTemplateMoreThanOneDecimal(this).Create("Drain rate", diff.DrainRate); + if (HasMoreThanOneDecimalPlace("Drain rate", diff.DrainRate, out issue)) + yield return issue; - if (OutOfRange(diff.DrainRate)) - yield return new IssueTemplateOutOfRange(this).Create("Drain rate", diff.DrainRate); + if (OutOfRange("Drain rate", diff.DrainRate, out issue)) + yield return issue; } } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs index 93592a866be7..638f0cfd537e 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAbnormalDifficultySettings.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks @@ -21,14 +22,18 @@ public abstract class CheckAbnormalDifficultySettings : ICheck /// /// If the setting is out of the boundaries set by the editor (0 - 10) /// - protected bool OutOfRange(float setting) + protected bool OutOfRange(string setting, float value, [NotNullWhen(true)] out Issue? issue) { - return setting < 0f || setting > 10f; + bool hasIssue = value < 0f || value > 10f; + issue = hasIssue ? new IssueTemplateOutOfRange(this).Create(setting, value) : null; + return hasIssue; } - protected bool HasMoreThanOneDecimalPlace(float setting) + protected bool HasMoreThanOneDecimalPlace(string setting, float value, [NotNullWhen(true)] out Issue? issue) { - return float.Round(setting, 1) != setting; + bool hasIssue = float.Round(value, 1) != value; + issue = hasIssue ? new IssueTemplateMoreThanOneDecimal(this).Create(setting, value) : null; + return hasIssue; } public class IssueTemplateMoreThanOneDecimal : IssueTemplate