2018-04-13 17:19:50 +08:00
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System ;
using System.Linq ;
2018-08-24 22:57:44 +08:00
using JetBrains.Annotations ;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps ;
using osu.Game.Rulesets.Mania.MathUtils ;
using osu.Game.Rulesets.Objects ;
using OpenTK ;
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 FastRandom Random ;
/// <summary>
/// The beatmap which <see cref="HitObject"/> is being converted from.
/// </summary>
2018-04-19 19:44:38 +08:00
protected readonly IBeatmap OriginalBeatmap ;
2018-04-13 17:19:50 +08:00
2018-04-19 19:44:38 +08:00
protected PatternGenerator ( FastRandom random , HitObject hitObject , ManiaBeatmap beatmap , Pattern previousPattern , IBeatmap originalBeatmap )
2018-04-13 17:19:50 +08:00
: base ( hitObject , beatmap , previousPattern )
{
if ( random = = null ) throw new ArgumentNullException ( nameof ( random ) ) ;
if ( originalBeatmap = = null ) throw new ArgumentNullException ( nameof ( originalBeatmap ) ) ;
Random = random ;
OriginalBeatmap = originalBeatmap ;
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 MathHelper . Clamp ( ( int ) Math . Floor ( position / local_x_divisor ) , 0 , 6 ) + 1 ;
}
float localXDivisor = 512f / TotalColumns ;
return MathHelper . Clamp ( ( int ) Math . 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 ;
2018-08-24 22:57:44 +08:00
2018-04-13 17:19:50 +08:00
/// <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 = OriginalBeatmap . HitObjects . LastOrDefault ( ) ;
HitObject firstObject = OriginalBeatmap . HitObjects . FirstOrDefault ( ) ;
2018-06-15 19:49:11 +08:00
// Drain time in seconds
int drainTime = ( int ) ( ( ( lastObject ? . StartTime ? ? 0 ) - ( firstObject ? . StartTime ? ? 0 ) - OriginalBeatmap . TotalBreakTime ) / 1000 ) ;
2018-04-13 17:19:50 +08:00
if ( drainTime = = 0 )
2018-07-06 12:29:40 +08:00
drainTime = 10000 ;
2018-04-13 17:19:50 +08:00
BeatmapDifficulty difficulty = OriginalBeatmap . BeatmapInfo . BaseDifficulty ;
2018-10-11 18:53:07 +08:00
conversionDifficulty = ( ( difficulty . DrainRate + MathHelper . Clamp ( difficulty . ApproachRate , 4 , 7 ) ) / 1.5 + ( double ) OriginalBeatmap . HitObjects . Count / drainTime * 9f ) / 38f * 5f / 1.15 ;
2018-04-13 17:19:50 +08:00
conversionDifficulty = Math . Min ( conversionDifficulty . Value , 12 ) ;
return conversionDifficulty . Value ;
}
}
2018-08-24 22:57:44 +08:00
/// <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="PatternGenerator.TotalColumns"/> 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 = 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 ) ) ;
}
/// <summary>
2018-08-24 23:07:48 +08:00
/// Returns a random column index in the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
2018-08-24 22:57:44 +08:00
/// </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="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." )
{
}
}
2018-04-13 17:19:50 +08:00
}
}