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; }
}
}