// Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; using System.Collections.Generic; using System.Linq; using osu.Framework.MathUtils; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { /// /// A pattern generator for IHasDistance hit objects. /// internal class DistanceObjectPatternGenerator : PatternGenerator { /// /// Base osu! slider scoring distance. /// private const float osu_base_scoring_distance = 100; public readonly double EndTime; public readonly double SegmentDuration; private readonly int spanCount; private PatternType convertType; public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { convertType = PatternType.None; if (!Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) convertType = PatternType.LowProbability; var distanceData = hitObject as IHasDistance; var repeatsData = hitObject as IHasRepeats; spanCount = repeatsData?.SpanCount() ?? 1; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime); // The true distance, accounting for any repeats double distance = (distanceData?.Distance ?? 0) * spanCount; // The velocity of the osu! hit object - calculated as the velocity of a slider double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength; // The duration of the osu! hit object double osuDuration = distance / osuVelocity; EndTime = hitObject.StartTime + osuDuration; SegmentDuration = (EndTime - HitObject.StartTime) / spanCount; } public override IEnumerable Generate() { var originalPattern = generate(); if (originalPattern.HitObjects.Count() == 1) { yield return originalPattern; yield break; } // We need to split the intermediate pattern into two new patterns: // 1. A pattern containing all objects that do not end at our EndTime. // 2. A pattern containing all objects that end at our EndTime. This will be used for further pattern generation. var intermediatePattern = new Pattern(); var endTimePattern = new Pattern(); foreach (var obj in originalPattern.HitObjects) { if (!Precision.AlmostEquals(EndTime, (obj as IHasEndTime)?.EndTime ?? obj.StartTime)) intermediatePattern.Add(obj); else endTimePattern.Add(obj); } yield return intermediatePattern; yield return endTimePattern; } private Pattern generate() { if (TotalColumns == 1) { var pattern = new Pattern(); addToPattern(pattern, 0, HitObject.StartTime, EndTime); return pattern; } if (spanCount > 1) { if (SegmentDuration <= 90) return generateRandomHoldNotes(HitObject.StartTime, 1); if (SegmentDuration <= 120) { convertType |= PatternType.ForceNotStack; return generateRandomNotes(HitObject.StartTime, spanCount + 1); } if (SegmentDuration <= 160) return generateStair(HitObject.StartTime); if (SegmentDuration <= 200 && ConversionDifficulty > 3) return generateRandomMultipleNotes(HitObject.StartTime); double duration = EndTime - HitObject.StartTime; if (duration >= 4000) return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0); if (SegmentDuration > 400 && spanCount < TotalColumns - 1 - RandomStart) return generateTiledHoldNotes(HitObject.StartTime); return generateHoldAndNormalNotes(HitObject.StartTime); } if (SegmentDuration <= 110) { if (PreviousPattern.ColumnWithObjects < TotalColumns) convertType |= PatternType.ForceNotStack; else convertType &= ~PatternType.ForceNotStack; return generateRandomNotes(HitObject.StartTime, SegmentDuration < 80 ? 1 : 2); } if (ConversionDifficulty > 6.5) { if ((convertType & PatternType.LowProbability) > 0) return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0); return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03); } if (ConversionDifficulty > 4) { if ((convertType & PatternType.LowProbability) > 0) return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0); return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0); } if (ConversionDifficulty > 2.5) { if ((convertType & PatternType.LowProbability) > 0) return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0); return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0); } if ((convertType & PatternType.LowProbability) > 0) return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0); return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0); } /// /// Generates random hold notes that start at an span the same amount of rows. /// /// Start time of each hold note. /// Number of hold notes. /// The containing the hit objects. private Pattern generateRandomHoldNotes(double startTime, int noteCount) { // - - - - // ■ - ■ ■ // □ - □ □ // ■ - ■ ■ var pattern = new Pattern(); int usableColumns = TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects; int nextColumn = Random.Next(RandomStart, TotalColumns); for (int i = 0; i < Math.Min(usableColumns, noteCount); i++) { while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn)) //find available column nextColumn = Random.Next(RandomStart, TotalColumns); 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++) { while (pattern.ColumnHasObject(nextColumn)) nextColumn = Random.Next(RandomStart, TotalColumns); addToPattern(pattern, nextColumn, startTime, EndTime); } return pattern; } /// /// Generates random notes, with one note per row and no stacking. /// /// The start time. /// The number of notes. /// The containing the hit objects. private Pattern generateRandomNotes(double startTime, int noteCount) { // - - - - // x - - - // - - x - // - - - x // x - - - var pattern = new Pattern(); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if ((convertType & PatternType.ForceNotStack) > 0 && PreviousPattern.ColumnWithObjects < TotalColumns) { while (PreviousPattern.ColumnHasObject(nextColumn)) nextColumn = Random.Next(RandomStart, TotalColumns); } int lastColumn = nextColumn; for (int i = 0; i < noteCount; i++) { addToPattern(pattern, nextColumn, startTime, startTime); while (nextColumn == lastColumn) nextColumn = Random.Next(RandomStart, TotalColumns); lastColumn = nextColumn; startTime += SegmentDuration; } return pattern; } /// /// Generates a stair of notes, with one note per row. /// /// The start time. /// The containing the hit objects. private Pattern generateStair(double startTime) { // - - - - // x - - - // - x - - // - - x - // - - - x // - - x - // - x - - // x - - - var pattern = new Pattern(); int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); bool increasing = Random.NextDouble() > 0.5; for (int i = 0; i <= spanCount; i++) { addToPattern(pattern, column, startTime, startTime); startTime += SegmentDuration; // Check if we're at the borders of the stage, and invert the pattern if so if (increasing) { if (column >= TotalColumns - 1) { increasing = false; column--; } else column++; } else { if (column <= RandomStart) { increasing = true; column++; } else column--; } } return pattern; } /// /// Generates random notes with 1-2 notes per row and no stacking. /// /// The start time. /// The containing the hit objects. private Pattern generateRandomMultipleNotes(double startTime) { // - - - - // x - - - // - x x - // - - - x // x - x - var pattern = new Pattern(); bool legacy = TotalColumns >= 4 && TotalColumns <= 8; int interval = Random.Next(1, TotalColumns - (legacy ? 1 : 0)); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); for (int i = 0; i <= spanCount; i++) { addToPattern(pattern, nextColumn, startTime, startTime); nextColumn += interval; if (nextColumn >= TotalColumns - RandomStart) nextColumn = nextColumn - TotalColumns - RandomStart + (legacy ? 1 : 0); nextColumn += RandomStart; // If we're in 2K, let's not add many consecutive doubles if (TotalColumns > 2) addToPattern(pattern, nextColumn, startTime, startTime); nextColumn = Random.Next(RandomStart, TotalColumns); startTime += SegmentDuration; } return pattern; } /// /// Generates random hold notes. The amount of hold notes generated is determined by probabilities. /// /// The hold note start time. /// The probability required for 2 hold notes to be generated. /// The probability required for 3 hold notes to be generated. /// The probability required for 4 hold notes to be generated. /// The containing the hit objects. private Pattern generateNRandomNotes(double startTime, double p2, double p3, double p4) { // - - - - // ■ - ■ ■ // □ - □ □ // ■ - ■ ■ switch (TotalColumns) { case 2: p2 = 0; p3 = 0; p4 = 0; break; case 3: p2 = Math.Min(p2, 0.1); p3 = 0; p4 = 0; break; case 4: p2 = Math.Min(p2, 0.3); p3 = Math.Min(p3, 0.04); p4 = 0; break; case 5: p2 = Math.Min(p2, 0.34); p3 = Math.Min(p3, 0.1); p4 = Math.Min(p4, 0.03); break; } bool isDoubleSample(SampleInfo sample) => sample.Name == SampleInfo.HIT_CLAP || sample.Name == SampleInfo.HIT_FINISH; bool canGenerateTwoNotes = (convertType & PatternType.LowProbability) == 0; canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample); if (canGenerateTwoNotes) p2 = 1; return generateRandomHoldNotes(startTime, GetRandomNoteCount(p2, p3, p4)); } /// /// Generates tiled hold notes. You can think of this as a stair of hold notes. /// /// The first hold note start time. /// The containing the hit objects. private Pattern generateTiledHoldNotes(double startTime) { // - - - - // ■ ■ ■ ■ // □ □ □ □ // □ □ □ □ // □ □ □ ■ // □ □ ■ - // □ ■ - - // ■ - - - var pattern = new Pattern(); int columnRepeat = Math.Min(spanCount, TotalColumns); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if ((convertType & PatternType.ForceNotStack) > 0 && PreviousPattern.ColumnWithObjects < TotalColumns) { while (PreviousPattern.ColumnHasObject(nextColumn)) nextColumn = Random.Next(RandomStart, TotalColumns); } for (int i = 0; i < columnRepeat; i++) { while (pattern.ColumnHasObject(nextColumn)) nextColumn = Random.Next(RandomStart, TotalColumns); addToPattern(pattern, nextColumn, startTime, EndTime); startTime += SegmentDuration; } return pattern; } /// /// Generates a hold note alongside normal notes. /// /// The start time of notes. /// The containing the hit objects. private Pattern generateHoldAndNormalNotes(double startTime) { // - - - - // ■ x x - // ■ - x x // ■ x - x // ■ - x x var pattern = new Pattern(); int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if ((convertType & PatternType.ForceNotStack) > 0 && PreviousPattern.ColumnWithObjects < TotalColumns) { while (PreviousPattern.ColumnHasObject(holdColumn)) holdColumn = Random.Next(RandomStart, TotalColumns); } // Create the hold note addToPattern(pattern, holdColumn, startTime, EndTime); int nextColumn = Random.Next(RandomStart, TotalColumns); int noteCount; if (ConversionDifficulty > 6.5) noteCount = GetRandomNoteCount(0.63, 0); else if (ConversionDifficulty > 4) noteCount = GetRandomNoteCount(TotalColumns < 6 ? 0.12 : 0.45, 0); else if (ConversionDifficulty > 2.5) noteCount = GetRandomNoteCount(TotalColumns < 6 ? 0 : 0.24, 0); else noteCount = 0; noteCount = Math.Min(TotalColumns - 1, noteCount); bool ignoreHead = !sampleInfoListAt(startTime).Any(s => s.Name == SampleInfo.HIT_WHISTLE || s.Name == SampleInfo.HIT_FINISH || s.Name == SampleInfo.HIT_CLAP); var rowPattern = new Pattern(); for (int i = 0; i <= spanCount; i++) { if (!(ignoreHead && startTime == HitObject.StartTime)) { for (int j = 0; j < noteCount; j++) { while (rowPattern.ColumnHasObject(nextColumn) || nextColumn == holdColumn) nextColumn = Random.Next(RandomStart, TotalColumns); addToPattern(rowPattern, nextColumn, startTime, startTime); } } pattern.Add(rowPattern); rowPattern.Clear(); startTime += SegmentDuration; } return pattern; } /// /// Retrieves the sample info list at a point in time. /// /// The time to retrieve the sample info list from. /// private List sampleInfoListAt(double time) { var curveData = HitObject as IHasCurve; if (curveData == null) return HitObject.Samples; double segmentTime = (EndTime - HitObject.StartTime) / spanCount; int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime); return curveData.RepeatSamples[index]; } /// /// Constructs and adds a note to a pattern. /// /// The pattern to add to. /// The column to add the note to. /// The start time of the note. /// The end time of the note (set to for a non-hold note). private void addToPattern(Pattern pattern, int column, double startTime, double endTime) { ManiaHitObject newObject; if (startTime == endTime) { newObject = new Note { StartTime = startTime, Samples = sampleInfoListAt(startTime), Column = column }; } else { var holdNote = new HoldNote { StartTime = startTime, Column = column, Duration = endTime - startTime, Head = { Samples = sampleInfoListAt(startTime) }, Tail = { Samples = sampleInfoListAt(endTime) } }; newObject = holdNote; } pattern.Add(newObject); } } }