// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using System.Linq; using JetBrains.Annotations; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { /// <summary> /// A pattern generator for legacy hit objects. /// </summary> internal abstract class PatternGenerator : Patterns.PatternGenerator { /// <summary> /// The column index at which to start generating random notes. /// </summary> protected readonly int RandomStart; /// <summary> /// The random number generator to use. /// </summary> protected readonly LegacyRandom Random; protected PatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns) : base(hitObject, beatmap, totalColumns, previousPattern) { ArgumentNullException.ThrowIfNull(random); Random = random; RandomStart = TotalColumns == 8 ? 1 : 0; } /// <summary> /// Converts an x-position into a column. /// </summary> /// <param name="position">The x-position.</param> /// <param name="allowSpecial">Whether to treat as 7K + 1.</param> /// <returns>The column.</returns> protected int GetColumn(float position, bool allowSpecial = false) { if (allowSpecial && TotalColumns == 8) { const float local_x_divisor = 512f / 7; return Math.Clamp((int)MathF.Floor(position / local_x_divisor), 0, 6) + 1; } float localXDivisor = 512f / TotalColumns; return Math.Clamp((int)MathF.Floor(position / localXDivisor), 0, TotalColumns - 1); } /// <summary> /// Generates a count of notes to be generated from probabilities. /// </summary> /// <param name="p2">Probability for 2 notes to be generated.</param> /// <param name="p3">Probability for 3 notes to be generated.</param> /// <param name="p4">Probability for 4 notes to be generated.</param> /// <param name="p5">Probability for 5 notes to be generated.</param> /// <param name="p6">Probability for 6 notes to be generated.</param> /// <returns>The amount of notes to be generated.</returns> protected int GetRandomNoteCount(double p2, double p3, double p4 = 0, double p5 = 0, double p6 = 0) { if (p2 < 0 || p2 > 1) throw new ArgumentOutOfRangeException(nameof(p2)); if (p3 < 0 || p3 > 1) throw new ArgumentOutOfRangeException(nameof(p3)); if (p4 < 0 || p4 > 1) throw new ArgumentOutOfRangeException(nameof(p4)); if (p5 < 0 || p5 > 1) throw new ArgumentOutOfRangeException(nameof(p5)); if (p6 < 0 || p6 > 1) throw new ArgumentOutOfRangeException(nameof(p6)); double val = Random.NextDouble(); if (val >= 1 - p6) return 6; if (val >= 1 - p5) return 5; if (val >= 1 - p4) return 4; if (val >= 1 - p3) return 3; return val >= 1 - p2 ? 2 : 1; } private double? conversionDifficulty; /// <summary> /// A difficulty factor used for various conversion methods from osu!stable. /// </summary> protected double ConversionDifficulty { get { if (conversionDifficulty != null) return conversionDifficulty.Value; HitObject lastObject = Beatmap.HitObjects.LastOrDefault(); HitObject firstObject = Beatmap.HitObjects.FirstOrDefault(); // Drain time in seconds int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - Beatmap.TotalBreakTime) / 1000); if (drainTime == 0) drainTime = 10000; IBeatmapDifficultyInfo difficulty = Beatmap.Difficulty; conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)Beatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); return conversionDifficulty.Value; } } /// <summary> /// Finds a new column in which a <see cref="HitObject"/> can be placed. /// This uses <see cref="GetRandomColumn"/> to pick the next candidate column. /// </summary> /// <param name="initialColumn">The initial column to test. This may be returned if it is already a valid column.</param> /// <param name="patterns">A list of patterns for which the validity of a column should be checked against. /// A column is not a valid candidate if a <see cref="HitObject"/> occupies the same column in any of the patterns.</param> /// <returns>A column for which there are no <see cref="HitObject"/>s in any of <paramref name="patterns"/> occupying the same column.</returns> /// <exception cref="NotEnoughColumnsException">If there are no valid candidate columns.</exception> protected int FindAvailableColumn(int initialColumn, params Pattern[] patterns) => FindAvailableColumn(initialColumn, null, patterns: patterns); /// <summary> /// Finds a new column in which a <see cref="HitObject"/> can be placed. /// </summary> /// <param name="initialColumn">The initial column to test. This may be returned if it is already a valid column.</param> /// <param name="nextColumn">A function to retrieve the next column. If null, a randomisation scheme will be used.</param> /// <param name="validation">A function to perform additional validation checks to determine if a column is a valid candidate for a <see cref="HitObject"/>.</param> /// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param> /// <param name="upperBound">The maximum column index. If null, <see cref="Patterns.PatternGenerator.TotalColumns">TotalColumns</see> is used.</param> /// <param name="patterns">A list of patterns for which the validity of a column should be checked against. /// A column is not a valid candidate if a <see cref="HitObject"/> occupies the same column in any of the patterns.</param> /// <returns>A column which has passed the <paramref name="validation"/> check and for which there are no /// <see cref="HitObject"/>s in any of <paramref name="patterns"/> occupying the same column.</returns> /// <exception cref="NotEnoughColumnsException">If there are no valid candidate columns.</exception> protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func<int, int> nextColumn = null, [InstantHandle] Func<int, bool> validation = null, params Pattern[] patterns) { lowerBound ??= RandomStart; upperBound ??= TotalColumns; 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) { if (validation?.Invoke(column) == false) return false; foreach (var p in patterns) { if (p.ColumnHasObject(column)) return false; } return true; } } /// <summary> /// Returns a random column index in the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>). /// </summary> /// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param> /// <param name="upperBound">The maximum column index. If null, <see cref="Patterns.PatternGenerator.TotalColumns"/> is used.</param> protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns); /// <summary> /// Occurs when mania conversion is stuck in an infinite loop unable to find columns to place new hitobjects in. /// </summary> public class NotEnoughColumnsException : Exception { public NotEnoughColumnsException() : base("There were not enough columns to complete conversion.") { } } } }