diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 010cf962cc..37a8062d75 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -173,26 +173,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
int usableColumns = TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects;
- int nextColumn = Random.Next(RandomStart, TotalColumns);
+ int nextColumn = GetRandomColumn();
for (int i = 0; i < Math.Min(usableColumns, noteCount); i++)
{
// Find available column
- RunWhile(() => pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn), () =>
- {
- nextColumn = Random.Next(RandomStart, TotalColumns);
- });
-
+ nextColumn = FindAvailableColumn(nextColumn, pattern, PreviousPattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
}
// This is can't be combined with the above loop due to RNG
for (int i = 0; i < noteCount - usableColumns; i++)
{
- RunWhile(() => pattern.ColumnHasObject(nextColumn), () =>
- {
- nextColumn = Random.Next(RandomStart, TotalColumns);
- });
-
+ nextColumn = FindAvailableColumn(nextColumn, pattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
}
@@ -217,23 +209,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
- {
- RunWhile(() => PreviousPattern.ColumnHasObject(nextColumn), () =>
- {
- nextColumn = Random.Next(RandomStart, TotalColumns);
- });
- }
+ nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
int lastColumn = nextColumn;
for (int i = 0; i < noteCount; i++)
{
addToPattern(pattern, nextColumn, startTime, startTime);
-
- RunWhile(() => nextColumn == lastColumn, () =>
- {
- nextColumn = Random.Next(RandomStart, TotalColumns);
- });
-
+ nextColumn = FindAvailableColumn(nextColumn, validation: c => c != lastColumn);
lastColumn = nextColumn;
startTime += SegmentDuration;
}
@@ -325,7 +307,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (TotalColumns > 2)
addToPattern(pattern, nextColumn, startTime, startTime);
- nextColumn = Random.Next(RandomStart, TotalColumns);
+ nextColumn = GetRandomColumn();
startTime += SegmentDuration;
}
@@ -404,20 +386,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
- {
- RunWhile(() => PreviousPattern.ColumnHasObject(nextColumn), () =>
- {
- nextColumn = Random.Next(RandomStart, TotalColumns);
- });
- }
+ nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
for (int i = 0; i < columnRepeat; i++)
{
- RunWhile(() => pattern.ColumnHasObject(nextColumn), () =>
- {
- nextColumn = Random.Next(RandomStart, TotalColumns);
- });
-
+ nextColumn = FindAvailableColumn(nextColumn, pattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
startTime += SegmentDuration;
}
@@ -442,17 +415,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
- {
- RunWhile(() => PreviousPattern.ColumnHasObject(holdColumn), () =>
- {
- holdColumn = Random.Next(RandomStart, TotalColumns);
- });
- }
+ holdColumn = FindAvailableColumn(holdColumn, PreviousPattern);
// Create the hold note
addToPattern(pattern, holdColumn, startTime, EndTime);
- int nextColumn = Random.Next(RandomStart, TotalColumns);
+ int nextColumn = GetRandomColumn();
int noteCount;
if (ConversionDifficulty > 6.5)
noteCount = GetRandomNoteCount(0.63, 0);
@@ -473,11 +441,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
for (int j = 0; j < noteCount; j++)
{
- RunWhile(() => rowPattern.ColumnHasObject(nextColumn) || nextColumn == holdColumn, () =>
- {
- nextColumn = Random.Next(RandomStart, TotalColumns);
- });
-
+ nextColumn = FindAvailableColumn(nextColumn, validation: c => c != holdColumn, patterns: rowPattern);
addToPattern(rowPattern, nextColumn, startTime, startTime);
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
index eae9a0fc3b..775a4145e6 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
@@ -39,34 +39,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
addToPattern(pattern, 0, generateHold);
break;
case 8:
- addToPattern(pattern, getNextRandomColumn(RandomStart), generateHold);
+ addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
break;
default:
if (TotalColumns > 0)
- addToPattern(pattern, getNextRandomColumn(0), generateHold);
+ addToPattern(pattern, GetRandomColumn(), generateHold);
break;
}
return pattern;
}
- ///
- /// Picks a random column after a column.
- ///
- /// The starting column.
- /// A random column after .
- private int getNextRandomColumn(int start)
- {
- int nextColumn = Random.Next(start, TotalColumns);
-
- RunWhile(() => PreviousPattern.ColumnHasObject(nextColumn), () =>
- {
- nextColumn = Random.Next(start, TotalColumns);
- });
-
- return nextColumn;
- }
-
///
/// Constructs and adds a note to a pattern.
///
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index b2b9fe2446..da1dd62cf5 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -231,22 +231,27 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
for (int i = 0; i < noteCount; i++)
{
- RunWhile(() => pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn) && !allowStacking, () =>
- {
- if (convertType.HasFlag(PatternType.Gathered))
- {
- nextColumn++;
- if (nextColumn == TotalColumns)
- nextColumn = RandomStart;
- }
- else
- nextColumn = Random.Next(RandomStart, TotalColumns);
- });
+ nextColumn = allowStacking
+ ? FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: pattern)
+ : FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: new[] { pattern, PreviousPattern });
addToPattern(pattern, nextColumn);
}
return pattern;
+
+ int getNextColumn(int last)
+ {
+ if (convertType.HasFlag(PatternType.Gathered))
+ {
+ last++;
+ if (last == TotalColumns)
+ last = RandomStart;
+ }
+ else
+ last = GetRandomColumn();
+ return last;
+ }
}
///
@@ -292,13 +297,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre);
int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2;
- int nextColumn = Random.Next(RandomStart, columnLimit);
+ int nextColumn = GetRandomColumn(upperBound: columnLimit);
for (int i = 0; i < noteCount; i++)
{
- RunWhile(() => pattern.ColumnHasObject(nextColumn), () =>
- {
- nextColumn = Random.Next(RandomStart, columnLimit);
- });
+ nextColumn = FindAvailableColumn(nextColumn, upperBound: columnLimit, patterns: pattern);
// Add normal note
addToPattern(pattern, nextColumn);
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index 55081e5822..7a160ed389 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
@@ -90,6 +91,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
private double? conversionDifficulty;
+
///
/// A difficulty factor used for various conversion methods from osu!stable.
///
@@ -116,5 +118,82 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return conversionDifficulty.Value;
}
}
+
+ ///
+ /// Finds a new column in which a can be placed.
+ /// This uses to pick the next candidate column.
+ ///
+ /// The initial column to test. This may be returned if it is already a valid column.
+ /// A list of patterns for which the validity of a column should be checked against.
+ /// A column is not a valid candidate if a occupies the same column in any of the patterns.
+ /// A column for which there are no s in any of occupying the same column.
+ /// If there are no valid candidate columns.
+ protected int FindAvailableColumn(int initialColumn, params Pattern[] patterns)
+ => FindAvailableColumn(initialColumn, null, patterns: patterns);
+
+ ///
+ /// Finds a new column in which a can be placed.
+ ///
+ /// The initial column to test. This may be returned if it is already a valid column.
+ /// A function to retrieve the next column. If null, a randomisation scheme will be used.
+ /// A function to perform additional validation checks to determine if a column is a valid candidate for a .
+ /// The minimum column index. If null, is used.
+ /// The maximum column index. If null, is used.
+ /// A list of patterns for which the validity of a column should be checked against.
+ /// A column is not a valid candidate if a occupies the same column in any of the patterns.
+ /// A column which has passed the check and for which there are no
+ /// s in any of occupying the same column.
+ /// If there are no valid candidate columns.
+ protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func nextColumn = null, [InstantHandle] Func validation = null,
+ params Pattern[] patterns)
+ {
+ lowerBound = lowerBound ?? RandomStart;
+ upperBound = upperBound ?? TotalColumns;
+ nextColumn = nextColumn ?? (_ => GetRandomColumn(lowerBound, upperBound));
+
+ // Check for the initial column
+ if (isValid(initialColumn))
+ return initialColumn;
+
+ // Ensure that we have at least one free column, so that an endless loop is avoided
+ bool hasValidColumns = false;
+ for (int i = lowerBound.Value; i < upperBound.Value; i++)
+ {
+ hasValidColumns = isValid(i);
+ if (hasValidColumns)
+ break;
+ }
+
+ if (!hasValidColumns)
+ throw new NotEnoughColumnsException();
+
+ // Iterate until a valid column is found. This is a random iteration in the default case.
+ do
+ {
+ initialColumn = nextColumn(initialColumn);
+ } while (!isValid(initialColumn));
+
+ return initialColumn;
+
+ bool isValid(int column) => validation?.Invoke(column) != false && !patterns.Any(p => p.ColumnHasObject(column));
+ }
+
+ ///
+ /// Returns a random column index in the range [RandomStart, TotalColumns).
+ ///
+ /// The minimum column index. If null, is used.
+ /// The maximum column index. If null, is used.
+ protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns);
+
+ ///
+ /// Occurs when mania conversion is stuck in an infinite loop unable to find columns to place new hitobjects in.
+ ///
+ public class NotEnoughColumnsException : Exception
+ {
+ public NotEnoughColumnsException()
+ : base("There were not enough columns to complete conversion.")
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
index e51cbcdc60..a42d57cdd1 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs
@@ -3,9 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
-using JetBrains.Annotations;
-using osu.Framework.Logging;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
@@ -15,14 +12,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
///
internal abstract class PatternGenerator
{
- ///
- /// An arbitrary maximum amount of iterations to perform in .
- /// The specific value is not super important - enough such that no false-positives occur.
- ///
- /// /b/933228 requires at least 23 iterations.
- ///
- private const int max_rng_iterations = 30;
-
///
/// The last pattern.
///
@@ -53,44 +42,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
TotalColumns = Beatmap.TotalColumns;
}
- protected void RunWhile([InstantHandle] Func condition, Action action)
- {
- int iterations = 0;
-
- while (condition())
- {
- if (iterations++ >= max_rng_iterations)
- {
- // log an error but don't throw. we want to continue execution.
- Logger.Error(new ExceededAllowedIterationsException(new StackTrace(0)),
- "Conversion encountered errors. The beatmap may not be correctly converted.");
- return;
- }
-
- action();
- }
- }
-
///
/// Generates the patterns for , each filled with hit objects.
///
/// The s containing the hit objects.
public abstract IEnumerable Generate();
-
- ///
- /// Denotes when a single conversion operation is in an infinitely looping state.
- ///
- public class ExceededAllowedIterationsException : Exception
- {
- private readonly string stackTrace;
-
- public ExceededAllowedIterationsException(StackTrace stackTrace)
- {
- this.stackTrace = stackTrace.ToString();
- }
-
- public override string StackTrace => stackTrace;
- public override string ToString() => $"{GetType().Name}: {Message}\r\n{StackTrace}";
- }
}
}