diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs index 9cd18d2d9f..0699f5d039 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs @@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects public Vector2 Position { get; set; } - public float X => Position.X; - public float Y => Position.Y; + public float X + { + get => Position.X; + set => Position = new Vector2(value, Y); + } + + public float Y + { + get => Position.Y; + set => Position = new Vector2(X, value); + } } } diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs index 0c22554e82..f938d26b26 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs @@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects public Vector2 Position { get; set; } - public float X => Position.X; - public float Y => Position.Y; + public float X + { + get => Position.X; + set => Position = new Vector2(value, Y); + } + + public float Y + { + get => Position.Y; + set => Position = new Vector2(X, value); + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 329055b3dd..2018fd5ea9 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -210,11 +210,27 @@ namespace osu.Game.Rulesets.Catch.Objects /// public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y; - float IHasXPosition.X => OriginalX; + float IHasXPosition.X + { + get => OriginalX; + set => OriginalX = value; + } - float IHasYPosition.Y => LegacyConvertedY; + float IHasYPosition.Y + { + get => LegacyConvertedY; + set => LegacyConvertedY = value; + } - Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY); + Vector2 IHasPosition.Position + { + get => new Vector2(OriginalX, LegacyConvertedY); + set + { + ((IHasXPosition)this).X = value.X; + ((IHasYPosition)this).Y = value.Y; + } + } #endregion } diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs index 25ad6b997d..c8c8867bc6 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs @@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects #region LegacyBeatmapEncoder - float IHasXPosition.X => Column; + float IHasXPosition.X + { + get => Column; + set => Column = (int)value; + } #endregion } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 1b0993b698..8c1bd6302e 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects set => position.Value = value; } - public float X => Position.X; - public float Y => Position.Y; + public float X + { + get => Position.X; + set => Position = new Vector2(value, Position.Y); + } + + public float Y + { + get => Position.Y; + set => Position = new Vector2(Position.X, value); + } public Vector2 StackedPosition => Position + StackOffset; diff --git a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs index 8a95d26782..cf498c7856 100644 --- a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs @@ -11,6 +11,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; using MemoryStream = System.IO.MemoryStream; @@ -50,6 +51,29 @@ namespace osu.Game.Tests.Beatmaps.IO AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001)); } + [Test] + public void TestFractionalObjectCoordinatesRounded() + { + IWorkingBeatmap beatmap = null!; + MemoryStream outStream = null!; + + // 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)); + + // Ensure exporter legacy conversion is correct + AddStep("export", () => + { + outStream = new MemoryStream(); + + new LegacyBeatmapExporter(LocalStorage) + .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null); + }); + + AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream)); + AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001)); + } + [Test] public void TestExportStability() { diff --git a/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz b/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz new file mode 100644 index 0000000000..5c5af368c8 Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz differ diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index eb48425588..24e752da31 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -42,7 +42,10 @@ namespace osu.Game.Database return null; using var contentStreamReader = new LineBufferedReader(contentStream); - var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); + + // FIRST_LAZER_VERSION is specified here to avoid flooring object coordinates on decode via `(int)` casts. + // we will be making integers out of them lower down, but in a slightly different manner (rounding rather than truncating) + var beatmapContent = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION).Decode(contentStreamReader); var workingBeatmap = new FlatWorkingBeatmap(beatmapContent); var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset); @@ -93,6 +96,12 @@ namespace osu.Game.Database hitObject.StartTime = Math.Floor(hitObject.StartTime); + if (hitObject is IHasXPosition hasXPosition) + hasXPosition.X = MathF.Round(hasXPosition.X); + + if (hitObject is IHasYPosition hasYPosition) + hasYPosition.Y = MathF.Round(hasYPosition.Y); + if (hitObject is not IHasPath hasPath) continue; // stable's hit object parsing expects the entire slider to use only one type of curve, diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index ced9b24ebf..091b0a1e6f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs @@ -21,9 +21,17 @@ namespace osu.Game.Rulesets.Objects.Legacy public int ComboOffset { get; set; } - public float X => Position.X; + public float X + { + get => Position.X; + set => Position = new Vector2(value, Position.Y); + } - public float Y => Position.Y; + public float Y + { + get => Position.Y; + set => Position = new Vector2(Position.X, value); + } public Vector2 Position { get; set; } diff --git a/osu.Game/Rulesets/Objects/Types/IHasPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasPosition.cs index 8948fe59a9..e9b3cc46eb 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasPosition.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPosition.cs @@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The starting position of the HitObject. /// - Vector2 Position { get; } + Vector2 Position { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs index 7e55b21050..18f1f996e3 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The starting X-position of this HitObject. /// - float X { get; } + float X { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs index d2561b10a7..dcaeaf594a 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The starting Y-position of this HitObject. /// - float Y { get; } + float Y { get; set; } } }