diff --git a/osu.Game.Rulesets.Mania/Beatmaps/DistanceObjectConversion.cs b/osu.Game.Rulesets.Mania/Beatmaps/DistanceObjectConversion.cs new file mode 100644 index 0000000000..c775c189e1 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/DistanceObjectConversion.cs @@ -0,0 +1,409 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using System; +using osu.Game.Rulesets.Mania.MathUtils; +using System.Linq; +using OpenTK; +using osu.Game.Database; +using osu.Game.Audio; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Mania.Beatmaps +{ + internal class DistanceObjectConversion : ObjectConversion + { + private readonly HitObject originalObject; + + private readonly double endTime; + private readonly int repeatCount; + + private LegacyConvertType convertType; + + public DistanceObjectConversion(HitObject originalObject, ObjectRow previousRow, FastRandom random, Beatmap beatmap) + : base(previousRow, random, beatmap) + { + this.originalObject = originalObject; + + ControlPoint overridePoint; + ControlPoint controlPoint = Beatmap.TimingInfo.TimingPointAt(originalObject.StartTime, out overridePoint); + + convertType = LegacyConvertType.None; + if ((overridePoint ?? controlPoint)?.KiaiMode == false) + convertType = LegacyConvertType.LowProbability; + + var distanceData = originalObject as IHasDistance; + var repeatsData = originalObject as IHasRepeats; + + endTime = distanceData?.EndTime ?? 0; + repeatCount = repeatsData?.RepeatCount ?? 1; + } + + public override ObjectRow GenerateConversion() + { + double segmentDuration = endTime / repeatCount; + + if (repeatCount > 1) + { + if (segmentDuration <= 90) + return generateRandomHoldNotes(originalObject.StartTime, endTime, 1); + + if (segmentDuration <= 120) + { + convertType |= LegacyConvertType.ForceNotStack; + return addRandomNotes(originalObject.StartTime, segmentDuration, repeatCount); + } + + if (segmentDuration <= 160) + return addStair(originalObject.StartTime, segmentDuration, repeatCount); + + if (segmentDuration <= 200 && conversionDifficulty > 3) + return addMultipleNotes(originalObject.StartTime, segmentDuration, repeatCount); + + double duration = endTime - originalObject.StartTime; + if (duration >= 4000) + return addNRandomNotes(originalObject.StartTime, endTime, 0.23, 0, 0); + + if (segmentDuration > 400 && duration < 4000 && repeatCount < AvailableColumns - 1 - RandomStart) + return generateTiledHoldNotes(originalObject.StartTime, segmentDuration, repeatCount); + + return generateLongAndNormalNotes(originalObject.StartTime, segmentDuration); + } + + if (segmentDuration <= 110) + { + if (PreviousRow.Columns < AvailableColumns) + convertType |= LegacyConvertType.ForceNotStack; + else + convertType &= ~LegacyConvertType.ForceNotStack; + return addRandomNotes(originalObject.StartTime, segmentDuration, segmentDuration < 80 ? 0 : 1); + } + + if (conversionDifficulty > 6.5) + { + if ((convertType & LegacyConvertType.LowProbability) > 0) + return addNRandomNotes(originalObject.StartTime, endTime, 0.78, 0.3, 0); + return addNRandomNotes(originalObject.StartTime, endTime, 0.85, 0.36, 0.03); + } + + if (conversionDifficulty > 4) + { + if ((convertType & LegacyConvertType.LowProbability) > 0) + return addNRandomNotes(originalObject.StartTime, endTime, 0.43, 0.08, 0); + return addNRandomNotes(originalObject.StartTime, endTime, 0.56, 0.18, 0); + } + + if (conversionDifficulty > 2.5) + { + if ((convertType & LegacyConvertType.LowProbability) > 0) + return addNRandomNotes(originalObject.StartTime, endTime, 0.3, 0, 0); + return addNRandomNotes(originalObject.StartTime, endTime, 0.37, 0.08, 0); + } + + if ((convertType & LegacyConvertType.LowProbability) > 0) + return addNRandomNotes(originalObject.StartTime, endTime, 0.17, 0, 0); + return addNRandomNotes(originalObject.StartTime, endTime, 0.27, 0, 0); + } + + /// + /// Adds random hold notes. + /// + /// Number of hold notes. + /// Start time of each hold note. + /// End time of the hold notes. + /// The new row. + private ObjectRow generateRandomHoldNotes(double startTime, double endTime, int count) + { + var newRow = new ObjectRow(); + + int usableColumns = AvailableColumns - RandomStart - PreviousRow.Columns; + int nextColumn = Random.Next(RandomStart, AvailableColumns); + for (int i = 0; i < Math.Min(usableColumns, count); i++) + { + while (newRow.IsTaken(nextColumn) || PreviousRow.IsTaken(nextColumn)) //find available column + nextColumn = Random.Next(RandomStart, AvailableColumns); + add(newRow, nextColumn, startTime, endTime, count); + } + + // This is can't be combined with the above loop due to RNG + for (int i = 0; i < count - usableColumns; i++) + { + while (newRow.IsTaken(nextColumn)) + nextColumn = Random.Next(RandomStart, AvailableColumns); + add(newRow, nextColumn, startTime, endTime, count); + } + + return newRow; + } + + /// + /// Adds random notes, with one note per row. No stacking. + /// + /// The start time. + /// The separation of notes between rows. + /// The number of rows. + /// The new row. + private ObjectRow addRandomNotes(double startTime, double separationTime, int repeatCount) + { + var newRow = new ObjectRow(); + + int nextColumn = GetColumn((originalObject as IHasXPosition)?.X ?? 0, true); + if ((convertType & LegacyConvertType.ForceNotStack) > 0 && PreviousRow.Columns < AvailableColumns) + { + while (PreviousRow.IsTaken(nextColumn)) + nextColumn = Random.Next(RandomStart, AvailableColumns); + } + + int lastColumn = nextColumn; + for (int i = 0; i <= repeatCount; i++) + { + add(newRow, nextColumn, startTime, startTime); + while (nextColumn == lastColumn) + nextColumn = Random.Next(RandomStart, AvailableColumns); + + lastColumn = nextColumn; + startTime += separationTime; + } + + return newRow; + } + + /// + /// Creates a stair of notes, with one note per row. + /// + /// The start time. + /// The separation of notes between rows. + /// The number of rows/notes. + /// The new row. + private ObjectRow addStair(double startTime, double separationTime, int repeatCount) + { + var newRow = new ObjectRow(); + + int column = GetColumn((originalObject as IHasXPosition)?.X ?? 0, true); + bool increasing = Random.NextDouble() > 0.5; + + for (int i = 0; i <= repeatCount; i++) + { + add(newRow, column, startTime, startTime); + startTime += separationTime; + + // Check if we're at the borders of the stage, and invert the pattern if so + if (increasing) + { + if (column >= AvailableColumns - 1) + { + increasing = false; + column--; + } + else + column++; + } + else + { + if (column <= RandomStart) + { + increasing = true; + column++; + } + else + column--; + } + } + + return newRow; + } + + /// + /// Adds random notes, with 1-2 notes per row. No stacking. + /// + /// The start time. + /// The separation of notes between rows. + /// The number of rows. + /// The new row. + private ObjectRow addMultipleNotes(double startTime, double separationTime, int repeatCount) + { + var newRow = new ObjectRow(); + + bool legacy = AvailableColumns >= 4 && AvailableColumns <= 8; + int interval = Random.Next(1, AvailableColumns - (legacy ? 1 : 0)); + + int nextColumn = GetColumn((originalObject as IHasXPosition)?.X ?? 0, true); + for (int i = 0; i <= repeatCount; i++) + { + add(newRow, nextColumn, startTime, startTime, 2); + + nextColumn += interval; + if (nextColumn >= AvailableColumns - RandomStart) + nextColumn = nextColumn - AvailableColumns - RandomStart + (legacy ? 1 : 0); + nextColumn += RandomStart; + + // If we're in 2K, let's not add many consecutive doubles + if (AvailableColumns > 2) + add(newRow, nextColumn, startTime, startTime, 2); + + nextColumn = Random.Next(RandomStart, AvailableColumns); + startTime += separationTime; + } + + return newRow; + } + + /// + /// Generates random hold notes. The amount of hold notes generated is determined by probabilities. + /// + /// The hold note start time. + /// The hold note end 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 new row. + private ObjectRow addNRandomNotes(double startTime, double endTime, double p2, double p3, double p4) + { + switch (AvailableColumns) + { + case 2: + p2 = 0; + p3 = 0; + p4 = 0; + break; + case 3: + p2 = Math.Max(p2, 0.1); + p3 = 0; + p4 = 0; + break; + case 4: + p2 = Math.Max(p2, 0.3); + p3 = Math.Max(p3, 0.04); + p4 = 0; + break; + case 5: + p2 = Math.Max(p2, 0.34); + p3 = Math.Max(p3, 0.1); + p4 = Math.Max(p4, 0.03); + break; + } + + Func isDoubleSample = sample => sample.Name == SampleInfo.HIT_CLAP && sample.Name == SampleInfo.HIT_FINISH; + + bool canGenerateTwoNotes = (convertType & LegacyConvertType.LowProbability) == 0; + canGenerateTwoNotes &= originalObject.Samples.Any(isDoubleSample) || sampleInfoListAt(originalObject.StartTime, originalObject.StartTime - endTime).Any(isDoubleSample); + + if (canGenerateTwoNotes) + p2 = 0; + + return generateRandomHoldNotes(startTime, endTime, GetRandomNoteCount(p2, p3, p4)); + } + + private ObjectRow generateTiledHoldNotes(double startTime, double separationTime, int noteCount) + { + var newRow = new ObjectRow(); + + int columnRepeat = Math.Min(noteCount, AvailableColumns); + + int nextColumn = GetColumn((originalObject as IHasXPosition)?.X ?? 0, true); + if ((convertType & LegacyConvertType.ForceNotStack) > 0 && PreviousRow.Columns < AvailableColumns) + { + while (PreviousRow.IsTaken(nextColumn)) + nextColumn = Random.Next(RandomStart, AvailableColumns); + } + + for (int i = 0; i < columnRepeat; i++) + { + while (newRow.IsTaken(nextColumn)) + nextColumn = Random.Next(RandomStart, AvailableColumns); + + add(newRow, nextColumn, startTime, endTime, noteCount); + startTime += separationTime; + } + + return newRow; + } + + private ObjectRow generateLongAndNormalNotes(double startTime, double separationTime) + { + var newRow = new ObjectRow(); + + int holdColumn = GetColumn((originalObject as IHasXPosition)?.X ?? 0, true); + if ((convertType & LegacyConvertType.ForceNotStack) > 0 && PreviousRow.Columns < AvailableColumns) + { + while (PreviousRow.IsTaken(holdColumn)) + holdColumn = Random.Next(RandomStart, AvailableColumns); + } + + // Create the hold note + add(newRow, holdColumn, startTime, separationTime * repeatCount); + + // Todo: Complete + } + + private void add(ObjectRow row, int column, double startTime, double endTime, int siblings = 1) + { + ManiaHitObject newObject; + + if (startTime == endTime) + { + newObject = new Note + { + StartTime = startTime, + Samples = originalObject.Samples, + Column = column + }; + } + else + { + newObject = new HoldNote + { + StartTime = startTime, + Samples = originalObject.Samples, + Column = column, + Duration = endTime - startTime + }; + } + + // Todo: Consider siblings and write sample volumes (probably at ManiaHitObject level) + + row.Add(newObject); + } + + private SampleInfoList sampleInfoListAt(double time, double separationTime) + { + var curveData = originalObject as IHasCurve; + + if (curveData == null) + return originalObject.Samples; + + int index = (int)(separationTime == 0 ? 0 : (time - originalObject.StartTime) / separationTime); + return curveData.RepeatSamples[index]; + } + + private double? _conversionDifficulty; + private double conversionDifficulty + { + get + { + if (_conversionDifficulty != null) + return _conversionDifficulty.Value; + + HitObject lastObject = Beatmap.HitObjects.LastOrDefault(); + HitObject firstObject = Beatmap.HitObjects.FirstOrDefault(); + + double drainTime = (lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0); + drainTime -= Beatmap.EventInfo.TotalBreakTime; + + if (drainTime == 0) + drainTime = 10000; + + BeatmapDifficulty difficulty = Beatmap.BeatmapInfo.Difficulty; + _conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + Beatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; + _conversionDifficulty = Math.Min(_conversionDifficulty.Value, 12); + + return _conversionDifficulty.Value; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/LegacyConvertType.cs b/osu.Game.Rulesets.Mania/Beatmaps/LegacyConvertType.cs new file mode 100644 index 0000000000..086bc89292 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/LegacyConvertType.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; + +namespace osu.Game.Rulesets.Mania.Beatmaps +{ + [Flags] + internal enum LegacyConvertType + { + None = 0, + /// + /// Keep the same as last row. + /// + ForceStack = 1, + /// + /// Keep different from last row. + /// + ForceNotStack = 2, + /// + /// Keep as single note at its original position. + /// + KeepSingle = 4, + /// + /// Use a lower random value. + /// + LowProbability = 8, + /// + /// Reserved. + /// + Alternate = 16, + /// + /// Ignore the repeat count. + /// + ForceSigSlider = 32, + /// + /// Convert slider to circle. + /// + ForceNotSlider = 64, + /// + /// Notes gathered together. + /// + Gathered = 128, + Mirror = 256, + /// + /// Change 0 -> 6. + /// + Reverse = 512, + /// + /// 1 -> 5 -> 1 -> 5 like reverse. + /// + Cycle = 1024, + /// + /// Next note will be at column + 1. + /// + Stair = 2048, + /// + /// Next note will be at column - 1. + /// + ReverseStair = 4096 + } +} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/LegacyConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/LegacyConverter.cs index 544f396f4a..ebc6ed2dcf 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/LegacyConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/LegacyConverter.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Objects.Types; using System; using System.Collections.Generic; using osu.Game.Rulesets.Mania.MathUtils; +using osu.Game.Beatmaps.Timing; namespace osu.Game.Rulesets.Mania.Beatmaps { @@ -17,28 +18,51 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// internal class LegacyConverter { + private const int max_previous_note_times = 7; + private readonly FastRandom random; + private readonly List previousNoteTimes; + private readonly bool[] previousNotes; + private readonly double lastNoteTime; + private readonly float lastNotePosition; + private readonly int availableColumns; private readonly float localXDivisor; private readonly Beatmap beatmap; - public LegacyConverter(Beatmap beatmap) + public LegacyConverter(ObjectRow previousrow, Beatmap beatmap) { this.beatmap = beatmap; int seed = (int)Math.Round(beatmap.BeatmapInfo.Difficulty.DrainRate + beatmap.BeatmapInfo.Difficulty.CircleSize) * 20 + (int)(beatmap.BeatmapInfo.Difficulty.OverallDifficulty * 41.2) + (int)Math.Round(beatmap.BeatmapInfo.Difficulty.ApproachRate); + random = new FastRandom(seed); availableColumns = (int)Math.Round(beatmap.BeatmapInfo.Difficulty.CircleSize); localXDivisor = 512.0f / availableColumns; + + previousNoteTimes = new List(max_previous_note_times); + previousNotes = new bool[availableColumns]; } public IEnumerable Convert(HitObject original) { + var maniaOriginal = original as ManiaHitObject; + if (maniaOriginal != null) + { + yield return maniaOriginal; + yield break; + } + if (beatmap.BeatmapInfo.RulesetID == 3) yield return generateSpecific(original); + else + { + foreach (ManiaHitObject c in generateConverted(original)) + yield return c; + } } private ManiaHitObject generateSpecific(HitObject original) @@ -59,12 +83,43 @@ namespace osu.Game.Rulesets.Mania.Beatmaps }; } - return new Note + if (positionData != null) { - StartTime = original.StartTime, - Samples = original.Samples, - Column = column - }; + return new Note + { + StartTime = original.StartTime, + Samples = original.Samples, + Column = column + }; + } + + return null; + } + + private IEnumerable generateConverted(HitObject original) + { + var endTimeData = original as IHasEndTime; + var distanceData = original as IHasDistance; + var positionData = original as IHasPosition; + + ObjectConversion conversion = null; + + if (distanceData != null) + conversion = new DistanceObjectConversion(distanceData, beatmap); + else if (endTimeData != null) + { + // Spinner + } + else if (positionData != null) + { + // Circle + } + + if (conversion == null) + yield break; + + foreach (ManiaHitObject obj in conversion.GenerateConversion()) + yield return obj; } private int getColumn(float position) => MathHelper.Clamp((int)Math.Floor(position / localXDivisor), 0, availableColumns - 1); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ObjectConversion.cs b/osu.Game.Rulesets.Mania/Beatmaps/ObjectConversion.cs new file mode 100644 index 0000000000..9dee1116e0 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/ObjectConversion.cs @@ -0,0 +1,77 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.MathUtils; +using System; + +namespace osu.Game.Rulesets.Mania.Beatmaps +{ + internal abstract class ObjectConversion + { + protected readonly int AvailableColumns; + protected readonly int RandomStart; + + protected ObjectRow PreviousRow; + protected readonly FastRandom Random; + protected readonly Beatmap Beatmap; + + protected ObjectConversion(ObjectRow previousRow, FastRandom random, Beatmap beatmap) + { + PreviousRow = previousRow; + Random = random; + Beatmap = beatmap; + + AvailableColumns = (int)Math.Round(beatmap.BeatmapInfo.Difficulty.CircleSize); + RandomStart = AvailableColumns == 8 ? 1 : 0; + } + + /// + /// Generates a new row filled with converted hit objects. + /// + /// The new row. + public abstract ObjectRow GenerateConversion(); + + /// + /// Converts an x-position into a column. + /// + /// The x-position. + /// Whether to treat as 7K + 1. + /// The column. + protected int GetColumn(float position, bool allowSpecial = false) + { + if (allowSpecial && AvailableColumns == 8) + { + const float local_x_divisor = 512f / 7; + return MathHelper.Clamp((int)Math.Floor(position / local_x_divisor), 0, 6) + 1; + } + + float localXDivisor = 512f / AvailableColumns; + return MathHelper.Clamp((int)Math.Floor(position / localXDivisor), 0, AvailableColumns - 1); + } + + /// + /// Generates a count of notes to be generated from probabilities. + /// + /// Probability for 2 notes to be generated. + /// Probability for 3 notes to be generated. + /// Probability for 4 notes to be generated. + /// Probability for 5 notes to be generated. + /// Probability for 6 notes to be generated. + /// The amount of notes to be generated. + protected int GetRandomNoteCount(double p2, double p3, double p4 = 1, double p5 = 1, double p6 = 1) + { + 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; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ObjectRow.cs b/osu.Game.Rulesets.Mania/Beatmaps/ObjectRow.cs new file mode 100644 index 0000000000..fe51c16bed --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/ObjectRow.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Mania.Objects; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Rulesets.Mania.Beatmaps +{ + internal class ObjectRow + { + private readonly List hitObjects = new List(); + public IEnumerable HitObjects => hitObjects; + + /// + /// Whether a column of this row has been taken. + /// + /// The column index. + /// Whether the column already contains a hit object. + public bool IsTaken(int column) => hitObjects.Exists(h => h.Column == column); + + /// + /// Amount of columns taken up by hit objects in this row. + /// + public int Columns => HitObjects.GroupBy(h => h.Column).Count(); + + /// + /// Adds a hit object to this row. + /// + /// The hit object to add. + public void Add(ManiaHitObject hitObject) => hitObjects.Add(hitObject); + + /// + /// Clears this row. + /// + public void Clear() => hitObjects.Clear(); + + /// + /// Removes a hit object from this row. + /// + /// The hit object to remove. + public bool Remove(ManiaHitObject hitObject) => hitObjects.Remove(hitObject); + } +} diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 781dc3e228..36c17ccf04 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -47,8 +47,12 @@ + + + +