1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-12 14:43:03 +08:00

Merge pull request #31734 from bdach/simplify-slider-encoding-logic

Fix multi-segment-type sliders getting mangled on legacy export
This commit is contained in:
Dean Herbert 2025-02-04 15:08:44 +09:00 committed by GitHub
commit 1e1fb5476e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 37 additions and 54 deletions

View File

@ -447,60 +447,31 @@ namespace osu.Game.Beatmaps.Formats
private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position) private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position)
{ {
PathType? lastType = null;
for (int i = 0; i < pathData.Path.ControlPoints.Count; i++) for (int i = 0; i < pathData.Path.ControlPoints.Count; i++)
{ {
PathControlPoint point = pathData.Path.ControlPoints[i]; PathControlPoint point = pathData.Path.ControlPoints[i];
// Note that lazer's encoding format supports specifying multiple curve types for a slider path, which is not supported by stable.
// Backwards compatibility with stable is handled by `LegacyBeatmapExporter` and `BezierConverter.ConvertToModernBezier()`.
if (point.Type != null) if (point.Type != null)
{ {
// We've reached a new (explicit) segment! switch (point.Type?.Type)
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1;
// Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.
if (i > 1)
{ {
// We need to use the absolute control point position to determine equality, otherwise floating point issues may arise. case SplineType.BSpline:
Vector2 p1 = position + pathData.Path.ControlPoints[i - 1].Position; writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|");
Vector2 p2 = position + pathData.Path.ControlPoints[i - 2].Position; break;
if ((int)p1.X == (int)p2.X && (int)p1.Y == (int)p2.Y) case SplineType.Catmull:
needsExplicitSegment = true; writer.Write("C|");
} break;
if (needsExplicitSegment) case SplineType.PerfectCurve:
{ writer.Write("P|");
switch (point.Type?.Type) break;
{
case SplineType.BSpline:
writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|");
break;
case SplineType.Catmull: case SplineType.Linear:
writer.Write("C|"); writer.Write("L|");
break; break;
case SplineType.PerfectCurve:
writer.Write("P|");
break;
case SplineType.Linear:
writer.Write("L|");
break;
}
lastType = point.Type;
}
else
{
// New segment with the same type - duplicate the control point
writer.Write(FormattableString.Invariant($"{position.X + point.Position.X}:{position.Y + point.Position.Y}|"));
} }
} }

View File

@ -120,18 +120,30 @@ namespace osu.Game.Database
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1 if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1
&& hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue; && hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue;
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); var convertedToBezier = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
// Truncate control points to integer positions
foreach (var pathControlPoint in newControlPoints)
{
pathControlPoint.Position = new Vector2(
(float)Math.Floor(pathControlPoint.Position.X),
(float)Math.Floor(pathControlPoint.Position.Y));
}
hasPath.Path.ControlPoints.Clear(); hasPath.Path.ControlPoints.Clear();
hasPath.Path.ControlPoints.AddRange(newControlPoints);
for (int i = 0; i < convertedToBezier.Count; i++)
{
var convertedPoint = convertedToBezier[i];
// Truncate control points to integer positions
var position = new Vector2(
(float)Math.Floor(convertedPoint.Position.X),
(float)Math.Floor(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,
// and thus we specify the Bézier curve type once ever at the start of the slider.
hasPath.Path.ControlPoints.Add(new PathControlPoint(position, i == 0 ? PathType.BEZIER : null));
// however, the Bézier path as output by the converter has multiple segments.
// `LegacyBeatmapEncoder` will attempt to encode this by emitting per-control-point curve type specs which don't do anything for stable.
// instead, stable expects control points that start a segment to be present in the path twice in succession.
if (convertedPoint.Type == PathType.BEZIER && i > 0)
hasPath.Path.ControlPoints.Add(new PathControlPoint(position));
}
} }
// Encode to legacy format // Encode to legacy format