diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 7479c3120a..fea9246035 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -4,19 +4,14 @@
#nullable enable
using System;
-using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
-using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
-using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -28,12 +23,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "It never gets boring!";
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
- private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
-
- ///
- /// Number of previous hitobjects to be shifted together when another object is being moved.
- ///
- private const int preceding_hitobjects_to_shift = 10;
private Random? rng;
@@ -42,330 +31,33 @@ namespace osu.Game.Rulesets.Osu.Mods
if (!(beatmap is OsuBeatmap osuBeatmap))
return;
- var hitObjects = osuBeatmap.HitObjects;
-
Seed.Value ??= RNG.Next();
rng = new Random((int)Seed.Value);
- var randomObjects = randomiseObjects(hitObjects);
+ var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects);
- applyRandomisation(hitObjects, randomObjects);
- }
-
- ///
- /// Randomise the position of each hit object and return a list of s describing how each hit object should be placed.
- ///
- /// A list of s to have their positions randomised.
- /// A list of s describing how each hit object should be placed.
- private List randomiseObjects(IEnumerable hitObjects)
- {
- Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects");
-
- var randomObjects = new List();
- RandomObjectInfo? previous = null;
float rateOfChangeMultiplier = 0;
- foreach (OsuHitObject hitObject in hitObjects)
+ foreach (var positionInfo in positionInfos)
{
- var current = new RandomObjectInfo(hitObject);
- randomObjects.Add(current);
-
// rateOfChangeMultiplier only changes every 5 iterations in a combo
// to prevent shaky-line-shaped streams
- if (hitObject.IndexInCurrentCombo % 5 == 0)
+ if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
- if (previous == null)
+ if (positionInfo == positionInfos.First())
{
- current.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
- current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
+ positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
+ positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
}
else
{
- current.DistanceFromPrevious = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
-
- // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
- // is proportional to the distance between the last and the current hit object
- // to allow jumps and prevent too sharp turns during streams.
-
- // Allow maximum jump angle when jump distance is more than half of playfield diagonal length
- current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.DistanceFromPrevious / (playfield_diagonal * 0.5f));
+ positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f));
}
-
- previous = current;
}
- return randomObjects;
- }
-
- ///
- /// Reposition the hit objects according to the information in .
- ///
- /// The hit objects to be repositioned.
- /// A list of describing how each hit object should be placed.
- private void applyRandomisation(IReadOnlyList hitObjects, IReadOnlyList randomObjects)
- {
- RandomObjectInfo? previous = null;
-
- for (int i = 0; i < hitObjects.Count; i++)
- {
- var hitObject = hitObjects[i];
-
- var current = randomObjects[i];
-
- if (hitObject is Spinner)
- {
- previous = null;
- continue;
- }
-
- computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null);
-
- // Move hit objects back into the playfield if they are outside of it
- Vector2 shift = Vector2.Zero;
-
- switch (hitObject)
- {
- case HitCircle circle:
- shift = clampHitCircleToPlayfield(circle, current);
- break;
-
- case Slider slider:
- shift = clampSliderToPlayfield(slider, current);
- break;
- }
-
- if (shift != Vector2.Zero)
- {
- var toBeShifted = new List();
-
- for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
- {
- // only shift hit circles
- if (!(hitObjects[j] is HitCircle)) break;
-
- toBeShifted.Add(hitObjects[j]);
- }
-
- if (toBeShifted.Count > 0)
- applyDecreasingShift(toBeShifted, shift);
- }
-
- previous = current;
- }
- }
-
- ///
- /// Compute the randomised position of a hit object while attempting to keep it inside the playfield.
- ///
- /// The representing the hit object to have the randomised position computed for.
- /// The representing the hit object immediately preceding the current one.
- /// The representing the hit object immediately preceding the one.
- private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious)
- {
- float previousAbsoluteAngle = 0f;
-
- if (previous != null)
- {
- Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
- Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
- previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
- }
-
- float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle;
-
- var posRelativeToPrev = new Vector2(
- current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
- current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
- );
-
- Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre;
-
- posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
-
- current.PositionRandomised = lastEndPosition + posRelativeToPrev;
- }
-
- ///
- /// Move the randomised position of a hit circle so that it fits inside the playfield.
- ///
- /// The deviation from the original randomised position in order to fit within the playfield.
- private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo)
- {
- var previousPosition = objectInfo.PositionRandomised;
- objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding(
- objectInfo.PositionRandomised,
- (float)circle.Radius
- );
-
- circle.Position = objectInfo.PositionRandomised;
-
- return objectInfo.PositionRandomised - previousPosition;
- }
-
- ///
- /// Moves the and all necessary nested s into the if they aren't already.
- ///
- /// The deviation from the original randomised position in order to fit within the playfield.
- private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo)
- {
- var possibleMovementBounds = calculatePossibleMovementBounds(slider);
-
- var previousPosition = objectInfo.PositionRandomised;
-
- // Clamp slider position to the placement area
- // If the slider is larger than the playfield, force it to stay at the original position
- float newX = possibleMovementBounds.Width < 0
- ? objectInfo.PositionOriginal.X
- : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
-
- float newY = possibleMovementBounds.Height < 0
- ? objectInfo.PositionOriginal.Y
- : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
-
- slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY);
- objectInfo.EndPositionRandomised = slider.EndPosition;
-
- shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal);
-
- return objectInfo.PositionRandomised - previousPosition;
- }
-
- ///
- /// Decreasingly shift a list of s by a specified amount.
- /// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
- ///
- /// The list of hit objects to be shifted.
- /// The amount to be shifted.
- private void applyDecreasingShift(IList hitObjects, Vector2 shift)
- {
- for (int i = 0; i < hitObjects.Count; i++)
- {
- var hitObject = hitObjects[i];
- // The first object is shifted by a vector slightly smaller than shift
- // The last object is shifted by a vector slightly larger than zero
- Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
-
- hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
- }
- }
-
- ///
- /// Calculates a which contains all of the possible movements of the slider (in relative X/Y coordinates)
- /// such that the entire slider is inside the playfield.
- ///
- ///
- /// If the slider is larger than the playfield, the returned may have negative width/height.
- ///
- private RectangleF calculatePossibleMovementBounds(Slider slider)
- {
- var pathPositions = new List();
- slider.Path.GetPathToProgress(pathPositions, 0, 1);
-
- float minX = float.PositiveInfinity;
- float maxX = float.NegativeInfinity;
-
- float minY = float.PositiveInfinity;
- float maxY = float.NegativeInfinity;
-
- // Compute the bounding box of the slider.
- foreach (var pos in pathPositions)
- {
- minX = MathF.Min(minX, pos.X);
- maxX = MathF.Max(maxX, pos.X);
-
- minY = MathF.Min(minY, pos.Y);
- maxY = MathF.Max(maxY, pos.Y);
- }
-
- // Take the circle radius into account.
- float radius = (float)slider.Radius;
-
- minX -= radius;
- minY -= radius;
-
- maxX += radius;
- maxY += radius;
-
- // Given the bounding box of the slider (via min/max X/Y),
- // the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
- // and the amount that it can move to the right is WIDTH - maxX.
- // Same calculation applies for the Y axis.
- float left = -minX;
- float right = OsuPlayfield.BASE_SIZE.X - maxX;
- float top = -minY;
- float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
-
- return new RectangleF(left, top, right - left, bottom - top);
- }
-
- ///
- /// Shifts all nested s and s by the specified shift.
- ///
- /// whose nested s and s should be shifted
- /// The the 's nested s and s should be shifted by
- private void shiftNestedObjects(Slider slider, Vector2 shift)
- {
- foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
- {
- if (!(hitObject is OsuHitObject osuHitObject))
- continue;
-
- osuHitObject.Position += shift;
- }
- }
-
- ///
- /// Clamp a position to playfield, keeping a specified distance from the edges.
- ///
- /// The position to be clamped.
- /// The minimum distance allowed from playfield edges.
- /// The clamped position.
- private Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
- {
- return new Vector2(
- Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
- Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
- );
- }
-
- private class RandomObjectInfo
- {
- ///
- /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
- ///
- ///
- /// of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
- ///
- ///
- /// If is 0, the player's cursor doesn't need to change its direction of movement when passing
- /// the previous object to reach this one.
- ///
- public float RelativeAngle { get; set; }
-
- ///
- /// The jump distance from the previous hit object to this one.
- ///
- ///
- /// of the first hit object in a beatmap is relative to the playfield center.
- ///
- public float DistanceFromPrevious { get; set; }
-
- public Vector2 PositionOriginal { get; }
- public Vector2 PositionRandomised { get; set; }
-
- public Vector2 EndPositionOriginal { get; }
- public Vector2 EndPositionRandomised { get; set; }
-
- public OsuHitObject HitObject { get; }
-
- public RandomObjectInfo(OsuHitObject hitObject)
- {
- PositionRandomised = PositionOriginal = hitObject.Position;
- EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
- HitObject = hitObject;
- }
+ osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
index 97a4b14a62..da73c2addb 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
@@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Utils
{
- public static class OsuHitObjectGenerationUtils
+ public static partial class OsuHitObjectGenerationUtils
{
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
new file mode 100644
index 0000000000..d1bc3b45df
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
@@ -0,0 +1,340 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osuTK;
+
+#nullable enable
+
+namespace osu.Game.Rulesets.Osu.Utils
+{
+ public static partial class OsuHitObjectGenerationUtils
+ {
+ ///
+ /// Number of previous hitobjects to be shifted together when an object is being moved.
+ ///
+ private const int preceding_hitobjects_to_shift = 10;
+
+ private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
+
+ ///
+ /// Generate a list of s containing information for how the given list of
+ /// s are positioned.
+ ///
+ /// A list of s to process.
+ /// A list of s describing how each hit object is positioned relative to the previous one.
+ public static List GeneratePositionInfos(IEnumerable hitObjects)
+ {
+ var positionInfos = new List();
+ Vector2 previousPosition = playfield_centre;
+ float previousAngle = 0;
+
+ foreach (OsuHitObject hitObject in hitObjects)
+ {
+ Vector2 relativePosition = hitObject.Position - previousPosition;
+ float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
+ float relativeAngle = absoluteAngle - previousAngle;
+
+ positionInfos.Add(new ObjectPositionInfo(hitObject)
+ {
+ RelativeAngle = relativeAngle,
+ DistanceFromPrevious = relativePosition.Length
+ });
+
+ previousPosition = hitObject.EndPosition;
+ previousAngle = absoluteAngle;
+ }
+
+ return positionInfos;
+ }
+
+ ///
+ /// Reposition the hit objects according to the information in .
+ ///
+ /// Position information for each hit object.
+ /// The repositioned hit objects.
+ public static List RepositionHitObjects(IEnumerable objectPositionInfos)
+ {
+ List workingObjects = objectPositionInfos.Select(o => new WorkingObject(o)).ToList();
+ WorkingObject? previous = null;
+
+ for (int i = 0; i < workingObjects.Count; i++)
+ {
+ var current = workingObjects[i];
+ var hitObject = current.HitObject;
+
+ if (hitObject is Spinner)
+ {
+ previous = null;
+ continue;
+ }
+
+ computeModifiedPosition(current, previous, i > 1 ? workingObjects[i - 2] : null);
+
+ // Move hit objects back into the playfield if they are outside of it
+ Vector2 shift = Vector2.Zero;
+
+ switch (hitObject)
+ {
+ case HitCircle _:
+ shift = clampHitCircleToPlayfield(current);
+ break;
+
+ case Slider _:
+ shift = clampSliderToPlayfield(current);
+ break;
+ }
+
+ if (shift != Vector2.Zero)
+ {
+ var toBeShifted = new List();
+
+ for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
+ {
+ // only shift hit circles
+ if (!(workingObjects[j].HitObject is HitCircle)) break;
+
+ toBeShifted.Add(workingObjects[j].HitObject);
+ }
+
+ if (toBeShifted.Count > 0)
+ applyDecreasingShift(toBeShifted, shift);
+ }
+
+ previous = current;
+ }
+
+ return workingObjects.Select(p => p.HitObject).ToList();
+ }
+
+ ///
+ /// Compute the modified position of a hit object while attempting to keep it inside the playfield.
+ ///
+ /// The representing the hit object to have the modified position computed for.
+ /// The representing the hit object immediately preceding the current one.
+ /// The representing the hit object immediately preceding the one.
+ private static void computeModifiedPosition(WorkingObject current, WorkingObject? previous, WorkingObject? beforePrevious)
+ {
+ float previousAbsoluteAngle = 0f;
+
+ if (previous != null)
+ {
+ Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
+ Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
+ previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
+ }
+
+ float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
+
+ var posRelativeToPrev = new Vector2(
+ current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
+ current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
+ );
+
+ Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
+
+ posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
+
+ current.PositionModified = lastEndPosition + posRelativeToPrev;
+ }
+
+ ///
+ /// Move the modified position of a so that it fits inside the playfield.
+ ///
+ /// The deviation from the original modified position in order to fit within the playfield.
+ private static Vector2 clampHitCircleToPlayfield(WorkingObject workingObject)
+ {
+ var previousPosition = workingObject.PositionModified;
+ workingObject.EndPositionModified = workingObject.PositionModified = clampToPlayfieldWithPadding(
+ workingObject.PositionModified,
+ (float)workingObject.HitObject.Radius
+ );
+
+ workingObject.HitObject.Position = workingObject.PositionModified;
+
+ return workingObject.PositionModified - previousPosition;
+ }
+
+ ///
+ /// Moves the and all necessary nested s into the if they aren't already.
+ ///
+ /// The deviation from the original modified position in order to fit within the playfield.
+ private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
+ {
+ var slider = (Slider)workingObject.HitObject;
+ var possibleMovementBounds = calculatePossibleMovementBounds(slider);
+
+ var previousPosition = workingObject.PositionModified;
+
+ // Clamp slider position to the placement area
+ // If the slider is larger than the playfield, force it to stay at the original position
+ float newX = possibleMovementBounds.Width < 0
+ ? workingObject.PositionOriginal.X
+ : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
+
+ float newY = possibleMovementBounds.Height < 0
+ ? workingObject.PositionOriginal.Y
+ : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
+
+ slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
+ workingObject.EndPositionModified = slider.EndPosition;
+
+ shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
+
+ return workingObject.PositionModified - previousPosition;
+ }
+
+ ///
+ /// Decreasingly shift a list of s by a specified amount.
+ /// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
+ ///
+ /// The list of hit objects to be shifted.
+ /// The amount to be shifted.
+ private static void applyDecreasingShift(IList hitObjects, Vector2 shift)
+ {
+ for (int i = 0; i < hitObjects.Count; i++)
+ {
+ var hitObject = hitObjects[i];
+ // The first object is shifted by a vector slightly smaller than shift
+ // The last object is shifted by a vector slightly larger than zero
+ Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
+
+ hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
+ }
+ }
+
+ ///
+ /// Calculates a which contains all of the possible movements of the slider (in relative X/Y coordinates)
+ /// such that the entire slider is inside the playfield.
+ ///
+ ///
+ /// If the slider is larger than the playfield, the returned may have negative width/height.
+ ///
+ private static RectangleF calculatePossibleMovementBounds(Slider slider)
+ {
+ var pathPositions = new List();
+ slider.Path.GetPathToProgress(pathPositions, 0, 1);
+
+ float minX = float.PositiveInfinity;
+ float maxX = float.NegativeInfinity;
+
+ float minY = float.PositiveInfinity;
+ float maxY = float.NegativeInfinity;
+
+ // Compute the bounding box of the slider.
+ foreach (var pos in pathPositions)
+ {
+ minX = MathF.Min(minX, pos.X);
+ maxX = MathF.Max(maxX, pos.X);
+
+ minY = MathF.Min(minY, pos.Y);
+ maxY = MathF.Max(maxY, pos.Y);
+ }
+
+ // Take the circle radius into account.
+ float radius = (float)slider.Radius;
+
+ minX -= radius;
+ minY -= radius;
+
+ maxX += radius;
+ maxY += radius;
+
+ // Given the bounding box of the slider (via min/max X/Y),
+ // the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
+ // and the amount that it can move to the right is WIDTH - maxX.
+ // Same calculation applies for the Y axis.
+ float left = -minX;
+ float right = OsuPlayfield.BASE_SIZE.X - maxX;
+ float top = -minY;
+ float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
+
+ return new RectangleF(left, top, right - left, bottom - top);
+ }
+
+ ///
+ /// Shifts all nested s and s by the specified shift.
+ ///
+ /// whose nested s and s should be shifted
+ /// The the 's nested s and s should be shifted by
+ private static void shiftNestedObjects(Slider slider, Vector2 shift)
+ {
+ foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
+ {
+ if (!(hitObject is OsuHitObject osuHitObject))
+ continue;
+
+ osuHitObject.Position += shift;
+ }
+ }
+
+ ///
+ /// Clamp a position to playfield, keeping a specified distance from the edges.
+ ///
+ /// The position to be clamped.
+ /// The minimum distance allowed from playfield edges.
+ /// The clamped position.
+ private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
+ {
+ return new Vector2(
+ Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
+ Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
+ );
+ }
+
+ public class ObjectPositionInfo
+ {
+ ///
+ /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
+ ///
+ ///
+ /// of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
+ ///
+ ///
+ /// If is 0, the player's cursor doesn't need to change its direction of movement when passing
+ /// the previous object to reach this one.
+ ///
+ public float RelativeAngle { get; set; }
+
+ ///
+ /// The jump distance from the previous hit object to this one.
+ ///
+ ///
+ /// of the first hit object in a beatmap is relative to the playfield center.
+ ///
+ public float DistanceFromPrevious { get; set; }
+
+ ///
+ /// The hit object associated with this .
+ ///
+ public OsuHitObject HitObject { get; }
+
+ public ObjectPositionInfo(OsuHitObject hitObject)
+ {
+ HitObject = hitObject;
+ }
+ }
+
+ private class WorkingObject
+ {
+ public Vector2 PositionOriginal { get; }
+ public Vector2 PositionModified { get; set; }
+ public Vector2 EndPositionModified { get; set; }
+
+ public ObjectPositionInfo PositionInfo { get; }
+ public OsuHitObject HitObject => PositionInfo.HitObject;
+
+ public WorkingObject(ObjectPositionInfo positionInfo)
+ {
+ PositionInfo = positionInfo;
+ PositionModified = PositionOriginal = HitObject.Position;
+ EndPositionModified = HitObject.EndPosition;
+ }
+ }
+ }
+}