From 3fbe777f3380cdde0ff7b1904166fe68dc603cd9 Mon Sep 17 00:00:00 2001 From: kennyaja <121273982+kennyaja@users.noreply.github.com> Date: Wed, 15 Oct 2025 22:52:07 +0700 Subject: [PATCH 1/5] Round instead of truncating slider control points to integer positions --- osu.Game/Database/LegacyBeatmapExporter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index e7e5ddb4d2..48b308716e 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -142,10 +142,10 @@ namespace osu.Game.Database { var convertedPoint = convertedToBezier[i]; - // Truncate control points to integer positions + // Round control points to integer positions var position = new Vector2( - (float)Math.Floor(convertedPoint.Position.X), - (float)Math.Floor(convertedPoint.Position.Y)); + (float)Math.Round(convertedPoint.Position.X), + (float)Math.Round(convertedPoint.Position.Y)); // stable only supports a single curve type specification per slider. // we exploit the fact that the converted-to-Bézier path only has Bézier segments, From cf1834b0807410312b7968464dc8728757e30ed2 Mon Sep 17 00:00:00 2001 From: kennyaja <121273982+kennyaja@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:25:43 +0700 Subject: [PATCH 2/5] Also round slider control points to integer positions if it has less than 2 segments --- osu.Game/Database/LegacyBeatmapExporter.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 48b308716e..93ac093cda 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -132,7 +132,19 @@ namespace osu.Game.Database hasPath.Path.ControlPoints[^1].Type = null; if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1 - && hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue; + && hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) + { + // Round every control point to integer positions before skipping to the next hit object + for (int i = 0; i < hasPath.Path.ControlPoints.Count; i++) + { + var position = new Vector2( + (float)Math.Round(hasPath.Path.ControlPoints[i].Position.X), + (float)Math.Round(hasPath.Path.ControlPoints[i].Position.Y)); + + hasPath.Path.ControlPoints[i].Position = position; + } + continue; + } var convertedToBezier = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); From 5f9ade661040c358d67ef9475ed09a8ddbce6855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Oct 2025 08:44:30 +0200 Subject: [PATCH 3/5] Fix code quality --- osu.Game/Database/LegacyBeatmapExporter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 93ac093cda..e24727d297 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -143,6 +143,7 @@ namespace osu.Game.Database hasPath.Path.ControlPoints[i].Position = position; } + continue; } From 8f8f6057487ab30ae830bf6600db9a4069507663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Oct 2025 08:44:58 +0200 Subject: [PATCH 4/5] Use `MathF` instead of casting --- osu.Game/Database/LegacyBeatmapExporter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index e24727d297..8d90c9adb4 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -138,8 +138,8 @@ namespace osu.Game.Database for (int i = 0; i < hasPath.Path.ControlPoints.Count; i++) { var position = new Vector2( - (float)Math.Round(hasPath.Path.ControlPoints[i].Position.X), - (float)Math.Round(hasPath.Path.ControlPoints[i].Position.Y)); + MathF.Round(hasPath.Path.ControlPoints[i].Position.X), + MathF.Round(hasPath.Path.ControlPoints[i].Position.Y)); hasPath.Path.ControlPoints[i].Position = position; } @@ -157,8 +157,8 @@ namespace osu.Game.Database // Round control points to integer positions var position = new Vector2( - (float)Math.Round(convertedPoint.Position.X), - (float)Math.Round(convertedPoint.Position.Y)); + MathF.Round(convertedPoint.Position.X), + MathF.Round(convertedPoint.Position.Y)); // stable only supports a single curve type specification per slider. // we exploit the fact that the converted-to-Bézier path only has Bézier segments, From 6e378ad5af84794ec37d148d6d983b0659be3954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Oct 2025 08:58:09 +0200 Subject: [PATCH 5/5] Update relevant test with new expectations --- .../Beatmaps/IO/LegacyBeatmapExporterTest.cs | 20 ++++++++++++++++-- .../Archives/fractional-coordinates.olz | Bin 556 -> 695 bytes 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs index cf498c7856..e1c385097f 100644 --- a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs @@ -59,7 +59,15 @@ namespace osu.Game.Tests.Beatmaps.IO // Ensure importer encoding is correct AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz")); - AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001)); + AddAssert("second slider has fractional position", + () => ((IHasXPosition)beatmap.Beatmap.HitObjects[1]).X, + () => Is.EqualTo(-3.0517578E-05).Within(0.00001)); + AddAssert("second slider path has fractional coordinates", + () => ((IHasPath)beatmap.Beatmap.HitObjects[1]).Path.ControlPoints[1].Position.X, + () => Is.EqualTo(191.999939).Within(0.00001)); + AddAssert("second hit circle has fractional position", + () => ((IHasYPosition)beatmap.Beatmap.HitObjects[3]).Y, + () => Is.EqualTo(383.99997).Within(0.00001)); // Ensure exporter legacy conversion is correct AddStep("export", () => @@ -71,7 +79,15 @@ namespace osu.Game.Tests.Beatmaps.IO }); AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream)); - AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001)); + AddAssert("second slider is snapped", + () => ((IHasXPosition)beatmap.Beatmap.HitObjects[1]).X, + () => Is.EqualTo(0).Within(0.00001)); + AddAssert("second slider path is snapped", + () => ((IHasPath)beatmap.Beatmap.HitObjects[1]).Path.ControlPoints[1].Position.X, + () => Is.EqualTo(192).Within(0.00001)); + AddAssert("second hit circle is snapped", + () => ((IHasYPosition)beatmap.Beatmap.HitObjects[3]).Y, + () => Is.EqualTo(384).Within(0.00001)); } [Test] diff --git a/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz b/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz index 5c5af368c8b95fe76c9f45f0dfbc5f1e73a126a5..40f33dea75f11e21e624e9bdb3332002db41e5f4 100644 GIT binary patch literal 695 zcmWIWW@Zs#VBp|j*y0u#UEs>Y_?3}?!IGJQ!GM8*Av7;LFTXrbL019B(tt6Xi&7Iy z@{2STqW$uVauai6_412LYXc7E-8SIa`&zqao~-kxwKqQM`tI81u!*gBJEL|{K+dwq zk38DeEb*Rbe>-vhmUxM$OSE?}PG$^#Qe9K?XuZ6>?{@BMHyk#Ym9381AYORx#$nTp z4XYwIeO%}ExsM|%cOl0V|HZkp=QK#J_!TD3n9j{z-Lky;%Ni5!cFx%|=9X=(eDyIX z_cohXf$N8R{#g+(L$=Orbh~i%VDPhLZ1U+3WNu9TS+`nWcZKis?)e+r)@@?gRdP%& z%#1$hkX6IbZJfocbT8THeM!6fk2~I z=MBzRsI@CuPJXxX)|^|vWZydO&O2Fg;xM=9)?Xi+^5z^}D6`b)QAvv3nMo>sha>uu z{A{-MvP5OXJs#`kVKXCJZ*E@y>`}*gC8UPUH+|HRB?Ktpt{0u?jQF< zHi@cP?lt?&obrQz${dx>2_JMO?o30IC#IQbOSJJW9 zhaS$DFr9tEf|)ladPXnUwY?!lG)1mBGvMk)*9azUF{95KH#1%c_(hk5%sIBW!zWbR zccI_jd7D>>O=v#3$NjmAN1lYeo}-dSUD*jQ(Fc!&dKmYHzvr%8-6$&^UUQ5|Y8_)r z-W0p{qL*EtwU%9xoTmCIY{uulGv>x;1MCcw^Sw@pm~LP8<6`>jbGDv;KCZJ^nY~t` z{%`KdR?|z`+R3^CTfTBEnqAj>Ow)IAphWCK-A|8~p6xBX-e_{RFJS&JX0a=u3^u4| ziT+mC;FoUQE62BTzS@=kzZYI9F4c?5>Py>O&-`wNip;Nz7T?-eE*3tXy5)YjCf6N@ zm8?p$*iP0zVGr