From 1a522a19f10ec9a56de41abc5a3eb8c29b76e366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Oct 2025 09:01:04 +0200 Subject: [PATCH] Disallow zero-length sliders from specifying a non-zero number of repeats (#35220) --- .../Objects/Legacy/ConvertHitObjectParser.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index c5a6c9e83d..3010373252 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -476,12 +476,30 @@ namespace osu.Game.Rulesets.Objects.Legacy private ConvertHitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, IList> nodeSamples) { + var path = new SliderPath(controlPoints, length); + + // there are known instances of beatmaps (https://osu.ppy.sh/beatmapsets/594828#osu/1258033) which contain zero-length sliders with non-zero numbers of repeats. + // this was exploiting a bug in stable in which the slider repeats would be generated as objects but never actually judged as a hit *or* miss during gameplay, + // therefore increasing the theoretical possible max combo to be gained from a slider while in practice never giving that extra combo. + // due to lazer ensuring that an object has its nested part fully judged, this would result in broken behaviours + // (either the zero-length slider giving hundreds of combo for nothing if the repeats are judged as hit, or insta-failing the player due to HP if judged as miss). + // to remedy this in a way that seems least damaging, detect this situation via a heuristic and reset the number of repeats to zero. + // this technically *does not* match stable beatmap parsing or conversion, *does not* match in-gameplay behaviour of such broken sliders, + // and *will* fail conversion mapping tests, but again, this is supposed to be a least-worst measure to prevent exploits. + // it is also applied centrally to all rulesets rather than in specific ruleset converters because this failure scenario + // translates across rulesets (osu! and catch are both affected). + if (Precision.AlmostEquals(path.Distance, 0)) + { + repeatCount = 0; + nodeSamples = [nodeSamples[0], nodeSamples[^1]]; + } + return lastObject = new ConvertSlider { Position = position, NewCombo = firstObject || lastObject is ConvertSpinner || newCombo, ComboOffset = newCombo ? comboOffset : 0, - Path = new SliderPath(controlPoints, length), + Path = path, NodeSamples = nodeSamples, RepeatCount = repeatCount };