// Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Taiko.Objects; using System; using System.Collections.Generic; using System.Linq; using osu.Game.IO.Serialization; using osu.Game.Audio; using osu.Game.Rulesets.Beatmaps; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Taiko.Beatmaps { internal class TaikoBeatmapConverter : BeatmapConverter { /// /// osu! is generally slower than taiko, so a factor is added to increase /// speed. This must be used everywhere slider length or beat length is used. /// private const float legacy_velocity_multiplier = 1.4f; /// /// Because swells are easier in taiko than spinners are in osu!, /// legacy taiko multiplies a factor when converting the number of required hits. /// private const float swell_hit_multiplier = 1.65f; /// /// Base osu! slider scoring distance. /// private const float osu_base_scoring_distance = 100; /// /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. /// private const float taiko_base_distance = 100; protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(HitObject) }; protected override Beatmap ConvertBeatmap(Beatmap original, bool isForCurrentRuleset) { // Rewrite the beatmap info to add the slider velocity multiplier BeatmapInfo info = original.BeatmapInfo.DeepClone(); info.Difficulty.SliderMultiplier *= legacy_velocity_multiplier; Beatmap converted = base.ConvertBeatmap(original, isForCurrentRuleset); // Post processing step to transform hit objects with the same start time into strong hits converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => { TaikoHitObject first = x.First(); if (x.Skip(1).Any()) first.IsStrong = true; return first; }).ToList(); return converted; } protected override IEnumerable ConvertHitObject(HitObject obj, Beatmap beatmap) { var distanceData = obj as IHasDistance; var repeatsData = obj as IHasRepeats; var endTimeData = obj as IHasEndTime; var curveData = obj as IHasCurve; // Old osu! used hit sounding to determine various hit type information SampleInfoList samples = obj.Samples; bool strong = samples.Any(s => s.Name == SampleInfo.HIT_FINISH); if (distanceData != null) { int repeats = repeatsData?.RepeatCount ?? 1; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime); double speedAdjustment = difficultyPoint.SpeedMultiplier; double speedAdjustedBeatLength = timingPoint.BeatLength * speedAdjustment; // The true distance, accounting for any repeats. This ends up being the drum roll distance later double distance = distanceData.Distance * repeats * legacy_velocity_multiplier; // The velocity of the taiko hit object - calculated as the velocity of a drum roll double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength; // The duration of the taiko hit object double taikoDuration = distance / taikoVelocity; // For some reason, old osu! always uses speedAdjustment to determine the taiko velocity, but // only uses it to determine osu! velocity if beatmap version < 8. Let's account for that here. if (beatmap.BeatmapInfo.BeatmapVersion >= 8) speedAdjustedBeatLength /= speedAdjustment; // The velocity of the osu! hit object - calculated as the velocity of a slider double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength; // The duration of the osu! hit object double osuDuration = distance / osuVelocity; // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.Difficulty.SliderTickRate, taikoDuration / repeats); if (tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength) { List allSamples = curveData != null ? curveData.RepeatSamples : new List(new[] { samples }); int i = 0; for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing) { SampleInfoList currentSamples = allSamples[i]; bool isRim = currentSamples.Any(s => s.Name == SampleInfo.HIT_CLAP || s.Name == SampleInfo.HIT_WHISTLE); strong = currentSamples.Any(s => s.Name == SampleInfo.HIT_FINISH); if (isRim) { yield return new RimHit { StartTime = j, Samples = currentSamples, IsStrong = strong }; } else { yield return new CentreHit { StartTime = j, Samples = currentSamples, IsStrong = strong, }; } i = (i + 1) % allSamples.Count; } } else { yield return new DrumRoll { StartTime = obj.StartTime, Samples = obj.Samples, IsStrong = strong, Duration = taikoDuration, TickRate = beatmap.BeatmapInfo.Difficulty.SliderTickRate == 3 ? 3 : 4, }; } } else if (endTimeData != null) { double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.Difficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; yield return new Swell { StartTime = obj.StartTime, Samples = obj.Samples, IsStrong = strong, Duration = endTimeData.Duration, RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier), }; } else { bool isRim = samples.Any(s => s.Name == SampleInfo.HIT_CLAP || s.Name == SampleInfo.HIT_WHISTLE); if (isRim) { yield return new RimHit { StartTime = obj.StartTime, Samples = obj.Samples, IsStrong = strong, }; } else { yield return new CentreHit { StartTime = obj.StartTime, Samples = obj.Samples, IsStrong = strong, }; } } } } }