// 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.Diagnostics; using System.Linq; using osu.Framework.Extensions.EnumExtensions; 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; using osu.Game.Beatmaps.Formats; 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 int StartTime; public readonly int EndTime; public readonly int SegmentDuration; public 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; Debug.Assert(distanceData != null); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime); double beatLength; #pragma warning disable 618 if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint) #pragma warning restore 618 beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; else beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier; SpanCount = repeatsData?.SpanCount() ?? 1; StartTime = (int)Math.Round(hitObject.StartTime); // This matches stable's calculation. EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier); SegmentDuration = (EndTime - 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 (EndTime != (int)Math.Round(obj.GetEndTime())) 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, StartTime, EndTime); return pattern; } if (SpanCount > 1) { if (SegmentDuration <= 90) return generateRandomHoldNotes(StartTime, 1); if (SegmentDuration <= 120) { convertType |= PatternType.ForceNotStack; return generateRandomNotes(StartTime, SpanCount + 1); } if (SegmentDuration <= 160) return generateStair(StartTime); if (SegmentDuration <= 200 && ConversionDifficulty > 3) return generateRandomMultipleNotes(StartTime); double duration = EndTime - StartTime; if (duration >= 4000) return generateNRandomNotes(StartTime, 0.23, 0, 0); if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart) return generateTiledHoldNotes(StartTime); return generateHoldAndNormalNotes(StartTime); } if (SegmentDuration <= 110) { if (PreviousPattern.ColumnWithObjects < TotalColumns) convertType |= PatternType.ForceNotStack; else convertType &= ~PatternType.ForceNotStack; return generateRandomNotes(StartTime, SegmentDuration < 80 ? 1 : 2); } if (ConversionDifficulty > 6.5) { if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.78, 0.3, 0); return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03); } if (ConversionDifficulty > 4) { if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.43, 0.08, 0); return generateNRandomNotes(StartTime, 0.56, 0.18, 0); } if (ConversionDifficulty > 2.5) { if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.3, 0, 0); return generateNRandomNotes(StartTime, 0.37, 0.08, 0); } if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.17, 0, 0); return generateNRandomNotes(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(int startTime, int noteCount) { // - - - - // ■ - ■ ■ // □ - □ □ // ■ - ■ ■ var pattern = new Pattern(); int usableColumns = TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects; int nextColumn = GetRandomColumn(); for (int i = 0; i < Math.Min(usableColumns, noteCount); i++) { // Find available column 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++) { nextColumn = FindAvailableColumn(nextColumn, pattern); 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(int startTime, int noteCount) { // - - - - // x - - - // - - x - // - - - x // x - - - var pattern = new Pattern(); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); int lastColumn = nextColumn; for (int i = 0; i < noteCount; i++) { addToPattern(pattern, nextColumn, startTime, startTime); nextColumn = FindAvailableColumn(nextColumn, validation: c => c != lastColumn); 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(int 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(int 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 = GetRandomColumn(); 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(int 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; } static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH; bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability); canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(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(int startTime) { // - - - - // ■ ■ ■ ■ // □ □ □ □ // □ □ □ □ // □ □ □ ■ // □ □ ■ - // □ ■ - - // ■ - - - var pattern = new Pattern(); int columnRepeat = Math.Min(SpanCount, TotalColumns); // Due to integer rounding, this is not guaranteed to be the same as EndTime (the class-level variable). int endTime = startTime + SegmentDuration * SpanCount; int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); for (int i = 0; i < columnRepeat; i++) { nextColumn = FindAvailableColumn(nextColumn, pattern); 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(int startTime) { // - - - - // ■ x x - // ■ - x x // ■ x - x // ■ - x x var pattern = new Pattern(); int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) holdColumn = FindAvailableColumn(holdColumn, PreviousPattern); // Create the hold note addToPattern(pattern, holdColumn, startTime, EndTime); int nextColumn = GetRandomColumn(); 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 == HitSampleInfo.HIT_WHISTLE || s.Name == HitSampleInfo.HIT_FINISH || s.Name == HitSampleInfo.HIT_CLAP); var rowPattern = new Pattern(); for (int i = 0; i <= SpanCount; i++) { if (!(ignoreHead && startTime == StartTime)) { for (int j = 0; j < noteCount; j++) { nextColumn = FindAvailableColumn(nextColumn, validation: c => c != holdColumn, patterns: rowPattern); 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 IList sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples; /// /// Retrieves the list of node samples that occur at time greater than or equal to . /// /// The time to retrieve node samples at. private List> nodeSamplesAt(int time) { if (!(HitObject is IHasPathWithRepeats curveData)) return null; var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration; // avoid slicing the list & creating copies, if at all possible. return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList(); } /// /// 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, int startTime, int endTime) { ManiaHitObject newObject; if (startTime == endTime) { newObject = new Note { StartTime = startTime, Samples = sampleInfoListAt(startTime), Column = column }; } else { newObject = new HoldNote { StartTime = startTime, Duration = endTime - startTime, Column = column, Samples = HitObject.Samples, NodeSamples = nodeSamplesAt(startTime) }; } pattern.Add(newObject); } } }