ppy / osu

rhythm is just a *click* away!
https://osu.ppy.sh
MIT License
15.36k stars 2.29k forks source link

Random mod - broken difficulty calculation #29729

Closed altrevamp closed 1 month ago

altrevamp commented 2 months ago

Type

Game behaviour

Bug description

Certain maps, usually around 5 range, gain a very high difficulty increase when selecting the Random mod. Sometimes it can jump up to 7-8 like in the first video provided.

The actual difficulty of the map with Random is clearly not 8* (shown in second video).

Screenshots or videos

https://github.com/user-attachments/assets/983f565e-8cda-46f6-a79b-7ab3b99d56e9

https://github.com/user-attachments/assets/78089047-d857-47d4-9f89-56bba0abd2fa

Version

2024.906.1

Logs

compressed-logs.zip

cihe13375 commented 2 months ago

For some reason, other mods that usually don't change the difficulty rating (hidden, traceable, autoplay) can increase or decrease the difficulty as well, also shown in the first video.

This is not a new behavior, I think it happens because the seed is recalculated when selecting a mod (if you manually specify a seed the SR would not change with e.g. HD anymore)

The high SR itself is likely a bug though, or at least the last release does not behave like this

Joehuu commented 2 months ago

For some reason, other mods that usually don't change the difficulty rating (hidden, traceable, autoplay) can increase or decrease the difficulty as well, also shown in the first video.

That is a duplicate of https://github.com/ppy/osu/issues/13453.

Leaving this open for the other issue.

altrevamp commented 2 months ago

I found that applying the Strict Tracking mod as well seems to correct the difficulty.

Edit: This led me to finding out that fast reverse sliders (and buzz sliders) cause a massive increase in pp. This might be causing the issue. First video shows a more extreme example of this, and the second video shows two reverse sliders adding 200+ pp.

https://github.com/user-attachments/assets/a9aee3db-32dd-4f77-8afa-c5d187235c15

https://github.com/user-attachments/assets/c5c92c32-68b3-48d3-95ce-9575a1dac69a

huyenden commented 2 months ago

Yeah I just played the random mod like you a few minutes ago and the star difficulty and pp reward increased which shocked me even though I knew it was a bug. That beatmap was only a 3 star map. Will probably just temporarily consider random a bug for a while.

bdach commented 1 month ago

This is a regression which bisects to https://github.com/ppy/osu/commit/f7cb6b9ed09bd041f3c21448abcb83c8de3ed09e, specifically this change:

https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game.Rulesets.Osu/Objects/Slider.cs#L257-L258

This breaks random specifically because random was doing its own things to attempt to compensate for the fact that moving the parent object did not adjust any other nesteds other than the head or tail:

https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs#L235 https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs#L310-L324

Thus, both the slider and random mod attempted to to their thing after the slider was moved, but ended up combining to do the wrong thing.

This is corroborated by applying the following diff:

diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
index a9ae313a31..d325a9c62a 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
@@ -319,6 +319,9 @@ private static void shiftNestedObjects(Slider slider, Vector2 shift)
                 if (!(hitObject is OsuHitObject osuHitObject))
                     continue;

+                if (hitObject == slider.NestedHitObjects.OfType<SliderRepeat>().LastOrDefault())
+                    continue;
+
                 osuHitObject.Position += shift;
             }
         }

which fixes the star rating (disclaimer: to get reliable reproduction I was testing on a single map, https://osu.ppy.sh/beatmapsets/1892718#osu/3903975, with a single fixed seed of 1337 - but I don't believe there is any reason to believe that the other cases reported don't pertain to the same scenario).

I have two actual proposals of fixes. Maybe even you could combine them into one total fix.

The safe one

In retrospect that Slider change is obviously horrible and I feel like an idiot for letting it pass review. Updating the position of only the last repeat is insane. And it's seemingly not even needed when you can write

diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs
index 9c2998466a..83278660d4 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs
@@ -63,7 +63,7 @@ protected override void Update()
             base.Update();

             var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle :
-                slider.RepeatCount % 2 == 0 ? slider.TailCircle : slider.LastRepeat!;
+                slider.RepeatCount % 2 == 0 ? slider.TailCircle : slider.HeadCircle;

             CirclePiece.UpdateFrom(circle);
             marker?.UpdateFrom(circle);
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 2b3bb18844..77e821fe7c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -253,9 +253,6 @@ private void updateNestedPositions()

             if (TailCircle != null)
                 TailCircle.Position = EndPosition;
-
-            if (LastRepeat != null)
-                LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1);
         }

         protected void UpdateNestedSamples()

which just gets rid of all that bad crap entirely and I have no reason to believe it wouldn't work.

The less safe one

The fact that that updateNestedPositions() flow doesn't touch all nested objects is also obviously bad. It can be improved, but the prerequisite would be storing the PathProgress of the nesteds. At this point the random mod hacks go away too:

diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 2b3bb18844..84bae75602 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -204,6 +204,7 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok
                             SpanStartTime = e.SpanStartTime,
                             StartTime = e.Time,
                             Position = Position + Path.PositionAt(e.PathProgress),
+                            PathProgress = e.PathProgress,
                             StackHeight = StackHeight,
                         });
                         break;
@@ -236,6 +237,7 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok
                             StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
                             Position = Position + Path.PositionAt(e.PathProgress),
                             StackHeight = StackHeight,
+                            PathProgress = e.PathProgress,
                         });
                         break;
                 }
@@ -254,8 +256,11 @@ private void updateNestedPositions()
             if (TailCircle != null)
                 TailCircle.Position = EndPosition;

-            if (LastRepeat != null)
-                LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1);
+            foreach (var repeat in NestedHitObjects.OfType<SliderRepeat>())
+                repeat.Position = Position + Path.PositionAt(repeat.PathProgress);
+
+            foreach (var tick in NestedHitObjects.OfType<SliderTick>())
+                tick.Position = Position + Path.PositionAt(tick.PathProgress);
         }

         protected void UpdateNestedSamples()
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
index e95cfd369d..1bbd1e8070 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs
@@ -5,6 +5,8 @@ namespace osu.Game.Rulesets.Osu.Objects
 {
     public class SliderRepeat : SliderEndCircle
     {
+        public double PathProgress { get; set; }
+
         public SliderRepeat(Slider slider)
             : base(slider)
         {
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
index 74ec4d6eb3..219c2be00b 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
@@ -13,6 +13,7 @@ public class SliderTick : OsuHitObject
     {
         public int SpanIndex { get; set; }
         public double SpanStartTime { get; set; }
+        public double PathProgress { get; set; }

         protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
         {
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
index a9ae313a31..f7a0543739 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
@@ -232,8 +232,6 @@ private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
             slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
             workingObject.EndPositionModified = slider.EndPosition;

-            shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
-
             return workingObject.PositionModified - previousPosition;
         }

@@ -307,22 +305,6 @@ public static RectangleF CalculatePossibleMovementBounds(Slider slider)
             return new RectangleF(left, top, right - left, bottom - top);
         }

-        /// <summary>
-        /// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
-        /// </summary>
-        /// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
-        /// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
-        private static void shiftNestedObjects(Slider slider, Vector2 shift)
-        {
-            foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
-            {
-                if (!(hitObject is OsuHitObject osuHitObject))
-                    continue;
-
-                osuHitObject.Position += shift;
-            }
-        }
-
         /// <summary>
         /// Clamp a position to playfield, keeping a specified distance from the edges.
         /// </summary>

This is rather untested (likely needs a full diffcalc run for a proper check), and there is a chance something else can go bad here, which is why I'm calling this less safe.

@ppy/team-client requesting feedback on the above re: your preferences. Maybe @OliBomby has something to say here too.

As a last thing I really think we should get some sort of automated scrutiny on all hitobject classes because it's the second time in recent memory that an editor change causes a diffcalc fail. Maybe a CODEOWNERS on the hitobject classes for automated review, maybe some github workflow to automatically run on any and all changes to hitobject classes.

cc @ppy/osu-pp-committee

OliBomby commented 1 month ago
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle :
                slider.RepeatCount % 2 == 0 ? slider.TailCircle : slider.HeadCircle;

This wouldn't work because when the repeat count is even, the tail circle and head circle are in the same place.

I prefer your 'less safe' solution.

peppy commented 1 month ago

I think I prefer the latter proposal as it is more likely to handle similar cases in the future.