2019-01-24 16:43:03 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
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 ;
2020-09-17 15:30:34 +08:00
using osu.Framework.Utils ;
2020-09-17 16:40:05 +08:00
using System.Threading ;
2018-04-13 17:19:50 +08:00
using osu.Game.Audio ;
using osu.Game.Beatmaps.ControlPoints ;
2020-07-13 16:06:00 +08:00
using osu.Game.Beatmaps.Formats ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Taiko.Beatmaps
{
internal class TaikoBeatmapConverter : BeatmapConverter < TaikoHitObject >
{
/// <summary>
/// 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.
/// </summary>
2020-04-21 15:45:01 +08:00
public const float LEGACY_VELOCITY_MULTIPLIER = 1.4f ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Because swells are easier in taiko than spinners are in osu!,
/// legacy taiko multiplies a factor when converting the number of required hits.
/// </summary>
private const float swell_hit_multiplier = 1.65f ;
/// <summary>
/// Base osu! slider scoring distance.
/// </summary>
private const float osu_base_scoring_distance = 100 ;
/// <summary>
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
/// </summary>
private const float taiko_base_distance = 100 ;
private readonly bool isForCurrentRuleset ;
2019-12-24 15:02:16 +08:00
public TaikoBeatmapConverter ( IBeatmap beatmap , Ruleset ruleset )
: base ( beatmap , ruleset )
2018-04-13 17:19:50 +08:00
{
2019-12-24 15:02:16 +08:00
isForCurrentRuleset = beatmap . BeatmapInfo . Ruleset . Equals ( ruleset . RulesetInfo ) ;
2018-04-13 17:19:50 +08:00
}
2019-12-23 16:44:18 +08:00
public override bool CanConvert ( ) = > true ;
2020-09-17 16:40:05 +08:00
protected override Beatmap < TaikoHitObject > ConvertBeatmap ( IBeatmap original , CancellationToken cancellationToken )
2018-04-13 17:19:50 +08:00
{
// Rewrite the beatmap info to add the slider velocity multiplier
2018-05-17 11:59:48 +08:00
original . BeatmapInfo = original . BeatmapInfo . Clone ( ) ;
original . BeatmapInfo . BaseDifficulty = original . BeatmapInfo . BaseDifficulty . Clone ( ) ;
2020-04-21 15:45:01 +08:00
original . BeatmapInfo . BaseDifficulty . SliderMultiplier * = LEGACY_VELOCITY_MULTIPLIER ;
2018-04-13 17:19:50 +08:00
2020-09-17 16:40:05 +08:00
Beatmap < TaikoHitObject > converted = base . ConvertBeatmap ( original , cancellationToken ) ;
2018-04-13 17:19:50 +08:00
if ( original . BeatmapInfo . RulesetID = = 3 )
{
// Post processing step to transform mania hit objects with the same start time into strong hits
converted . HitObjects = converted . HitObjects . GroupBy ( t = > t . StartTime ) . Select ( x = >
{
TaikoHitObject first = x . First ( ) ;
2020-10-09 16:12:01 +08:00
if ( x . Skip ( 1 ) . Any ( ) & & first . CanBeStrong )
2018-04-13 17:19:50 +08:00
first . IsStrong = true ;
return first ;
} ) . ToList ( ) ;
}
return converted ;
}
2020-09-17 16:40:05 +08:00
protected override IEnumerable < TaikoHitObject > ConvertHitObject ( HitObject obj , IBeatmap beatmap , CancellationToken cancellationToken )
2018-04-13 17:19:50 +08:00
{
// Old osu! used hit sounding to determine various hit type information
2019-11-08 13:04:57 +08:00
IList < HitSampleInfo > samples = obj . Samples ;
2018-04-13 17:19:50 +08:00
2019-06-30 20:58:30 +08:00
bool strong = samples . Any ( s = > s . Name = = HitSampleInfo . HIT_FINISH ) ;
2018-04-13 17:19:50 +08:00
2019-11-12 18:16:51 +08:00
switch ( obj )
2018-04-13 17:19:50 +08:00
{
2019-11-12 18:16:51 +08:00
case IHasDistance distanceData :
{
2020-07-13 16:06:00 +08:00
if ( shouldConvertSliderToHits ( obj , beatmap , distanceData , out var taikoDuration , out var tickSpacing ) )
2018-04-13 17:19:50 +08:00
{
2020-05-26 16:44:47 +08:00
List < IList < HitSampleInfo > > allSamples = obj is IHasPathWithRepeats curveData ? curveData . NodeSamples : new List < IList < HitSampleInfo > > ( new [ ] { samples } ) ;
2018-04-13 17:19:50 +08:00
2019-11-12 18:16:51 +08:00
int i = 0 ;
for ( double j = obj . StartTime ; j < = obj . StartTime + taikoDuration + tickSpacing / 8 ; j + = tickSpacing )
2018-04-13 17:19:50 +08:00
{
2019-11-12 18:16:51 +08:00
IList < HitSampleInfo > currentSamples = allSamples [ i ] ;
bool isRim = currentSamples . Any ( s = > s . Name = = HitSampleInfo . HIT_CLAP | | s . Name = = HitSampleInfo . HIT_WHISTLE ) ;
strong = currentSamples . Any ( s = > s . Name = = HitSampleInfo . HIT_FINISH ) ;
2020-03-23 11:08:15 +08:00
yield return new Hit
2018-04-13 17:19:50 +08:00
{
2020-03-23 11:08:15 +08:00
StartTime = j ,
Type = isRim ? HitType . Rim : HitType . Centre ,
Samples = currentSamples ,
IsStrong = strong
} ;
2019-11-12 18:16:51 +08:00
i = ( i + 1 ) % allSamples . Count ;
2020-09-18 12:06:41 +08:00
if ( Precision . AlmostEquals ( 0 , tickSpacing ) )
break ;
2018-04-13 17:19:50 +08:00
}
}
2019-11-12 18:16:51 +08:00
else
2018-04-13 17:19:50 +08:00
{
2019-11-12 18:16:51 +08:00
yield return new DrumRoll
{
StartTime = obj . StartTime ,
Samples = obj . Samples ,
IsStrong = strong ,
Duration = taikoDuration ,
TickRate = beatmap . BeatmapInfo . BaseDifficulty . SliderTickRate = = 3 ? 3 : 4
} ;
}
break ;
2018-04-13 17:19:50 +08:00
}
2020-05-27 11:38:39 +08:00
case IHasDuration endTimeData :
2018-04-13 17:19:50 +08:00
{
2019-11-12 18:16:51 +08:00
double hitMultiplier = BeatmapDifficulty . DifficultyRange ( beatmap . BeatmapInfo . BaseDifficulty . OverallDifficulty , 3 , 5 , 7.5 ) * swell_hit_multiplier ;
2018-04-13 17:19:50 +08:00
2019-11-12 18:16:51 +08:00
yield return new Swell
2018-04-13 17:19:50 +08:00
{
StartTime = obj . StartTime ,
Samples = obj . Samples ,
2019-11-12 18:16:51 +08:00
Duration = endTimeData . Duration ,
RequiredHits = ( int ) Math . Max ( 1 , endTimeData . Duration / 1000 * hitMultiplier )
2018-04-13 17:19:50 +08:00
} ;
2019-11-12 18:16:51 +08:00
break ;
2018-04-13 17:19:50 +08:00
}
2019-11-12 18:16:51 +08:00
default :
2018-04-13 17:19:50 +08:00
{
2020-05-11 11:53:54 +08:00
bool isRimDefinition ( HitSampleInfo s ) = > s . Name = = HitSampleInfo . HIT_CLAP | | s . Name = = HitSampleInfo . HIT_WHISTLE ;
bool isRim = samples . Any ( isRimDefinition ) ;
2020-03-23 11:08:15 +08:00
yield return new Hit
2019-11-12 18:16:51 +08:00
{
2020-03-23 11:08:15 +08:00
StartTime = obj . StartTime ,
Type = isRim ? HitType . Rim : HitType . Centre ,
2020-05-11 11:53:54 +08:00
Samples = samples ,
2020-03-23 11:08:15 +08:00
IsStrong = strong
} ;
2019-11-12 18:16:51 +08:00
break ;
2018-04-13 17:19:50 +08:00
}
}
}
2018-05-07 09:51:30 +08:00
2020-07-13 16:06:00 +08:00
private bool shouldConvertSliderToHits ( HitObject obj , IBeatmap beatmap , IHasDistance distanceData , out double taikoDuration , out double tickSpacing )
{
// DO NOT CHANGE OR REFACTOR ANYTHING IN HERE WITHOUT TESTING AGAINST _ALL_ BEATMAPS.
// Some of these calculations look redundant, but they are not - extremely small floating point errors are introduced to maintain 1:1 compatibility with stable.
// Rounding cannot be used as an alternative since the error deltas have been observed to be between 1e-2 and 1e-6.
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
int spans = ( obj as IHasRepeats ) ? . SpanCount ( ) ? ? 1 ;
double distance = distanceData . Distance * spans * LEGACY_VELOCITY_MULTIPLIER ;
TimingControlPoint timingPoint = beatmap . ControlPointInfo . TimingPointAt ( obj . StartTime ) ;
DifficultyControlPoint difficultyPoint = beatmap . ControlPointInfo . DifficultyPointAt ( obj . 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 ;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap . BeatmapInfo . BaseDifficulty . SliderMultiplier / beatmap . BeatmapInfo . BaseDifficulty . SliderTickRate ;
// The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
double taikoVelocity = sliderScoringPointDistance * beatmap . BeatmapInfo . BaseDifficulty . SliderTickRate ;
taikoDuration = distance / taikoVelocity * beatLength ;
if ( isForCurrentRuleset )
{
tickSpacing = 0 ;
return false ;
}
double osuVelocity = taikoVelocity * ( 1000f / beatLength ) ;
// osu-stable always uses the speed-adjusted beatlength to determine the osu! velocity, but only uses it for conversion if beatmap version < 8
if ( beatmap . BeatmapInfo . BeatmapVersion > = 8 )
beatLength = timingPoint . BeatLength ;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
tickSpacing = Math . Min ( beatLength / beatmap . BeatmapInfo . BaseDifficulty . SliderTickRate , taikoDuration / spans ) ;
return tickSpacing > 0
& & distance / osuVelocity * 1000 < 2 * beatLength ;
}
2018-05-07 09:51:30 +08:00
protected override Beatmap < TaikoHitObject > CreateBeatmap ( ) = > new TaikoBeatmap ( ) ;
2018-04-13 17:19:50 +08:00
}
}