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
2017-03-11 23:34:21 +08:00
using osu.Game.Beatmaps ;
2017-04-18 15:05:58 +08:00
using osu.Game.Rulesets.Objects ;
using osu.Game.Rulesets.Objects.Types ;
using osu.Game.Rulesets.Taiko.Objects ;
2017-04-03 09:54:13 +08:00
using System ;
2017-03-11 23:34:21 +08:00
using System.Collections.Generic ;
2017-03-17 13:39:38 +08:00
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 ;
2017-04-06 10:41:16 +08:00
using osu.Game.Audio ;
2017-05-23 12:55:18 +08:00
using osu.Game.Beatmaps.ControlPoints ;
2023-09-15 16:38:34 +08:00
using osu.Game.Rulesets.Objects.Legacy ;
2018-04-13 17:19:50 +08:00
2017-04-18 15:05:58 +08:00
namespace osu.Game.Rulesets.Taiko.Beatmaps
2017-03-11 23:34:21 +08:00
{
2017-04-18 08:38:52 +08:00
internal class TaikoBeatmapConverter : BeatmapConverter < TaikoHitObject >
2017-03-11 23:34:21 +08:00
{
2023-12-06 14:26:32 +08:00
/// <summary>
/// A speed multiplier applied globally to osu!taiko.
/// </summary>
/// <remarks>
/// osu! is generally slower than taiko, so a factor was historically added to increase speed for converts.
/// This must be used everywhere slider length or beat length is used in taiko.
///
/// Of note, this has never been exposed to the end user, and is considered a hidden internal multiplier.
/// </remarks>
public const float VELOCITY_MULTIPLIER = 1.4f ;
2017-04-03 19:32:03 +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 ;
2018-04-13 17:19:50 +08:00
2017-04-03 19:32:03 +08:00
/// <summary>
/// Base osu! slider scoring distance.
/// </summary>
private const float osu_base_scoring_distance = 100 ;
2018-04-13 17:19:50 +08:00
2017-08-22 13:21:28 +08:00
private readonly bool isForCurrentRuleset ;
2018-04-13 17:19:50 +08:00
2019-12-24 15:02:16 +08:00
public TaikoBeatmapConverter ( IBeatmap beatmap , Ruleset ruleset )
: base ( beatmap , ruleset )
2017-03-11 23:34:21 +08:00
{
2019-12-24 15:02:16 +08:00
isForCurrentRuleset = beatmap . BeatmapInfo . Ruleset . Equals ( ruleset . RulesetInfo ) ;
2017-08-22 13:18:17 +08:00
}
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 )
2017-08-22 13:18:17 +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
2022-10-27 13:25:50 +08:00
if ( original . BeatmapInfo . Ruleset . OnlineID = = 0 )
{
// Post processing step to transform standard slider velocity changes into scroll speed changes
double lastScrollSpeed = 1 ;
foreach ( HitObject hitObject in original . HitObjects )
{
2023-04-26 23:55:38 +08:00
if ( hitObject is not IHasSliderVelocity hasSliderVelocity ) continue ;
2023-09-06 17:59:15 +08:00
double nextScrollSpeed = hasSliderVelocity . SliderVelocityMultiplier ;
2022-10-28 05:41:17 +08:00
EffectControlPoint currentEffectPoint = converted . ControlPointInfo . EffectPointAt ( hitObject . StartTime ) ;
2022-10-27 13:25:50 +08:00
2022-10-28 05:41:17 +08:00
if ( ! Precision . AlmostEquals ( lastScrollSpeed , nextScrollSpeed , acceptableDifference : currentEffectPoint . ScrollSpeedBindable . Precision ) )
2022-10-27 13:25:50 +08:00
{
2022-10-28 05:41:17 +08:00
converted . ControlPointInfo . Add ( hitObject . StartTime , new EffectControlPoint
{
KiaiMode = currentEffectPoint . KiaiMode ,
ScrollSpeed = lastScrollSpeed = nextScrollSpeed ,
} ) ;
2022-10-27 13:25:50 +08:00
}
}
}
2022-01-27 14:19:48 +08:00
if ( original . BeatmapInfo . Ruleset . OnlineID = = 3 )
2017-03-11 23:34:21 +08:00
{
2017-10-01 06:15:23 +08:00
// 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-12-15 04:46:02 +08:00
if ( x . Skip ( 1 ) . Any ( ) & & first is TaikoStrongableHitObject strong )
2020-12-13 19:36:39 +08:00
strong . IsStrong = true ;
2017-10-01 06:15:23 +08:00
return first ;
} ) . ToList ( ) ;
}
2018-04-13 17:19:50 +08:00
2023-06-06 15:59:28 +08:00
// TODO: stable makes the last tick of a drumroll non-required when the next object is too close.
// This probably needs to be reimplemented:
//
// List<HitObject> hitobjects = hitObjectManager.hitObjects;
// int ind = hitobjects.IndexOf(this);
// if (i < hitobjects.Count - 1 && hitobjects[i + 1].HittableStartTime - (EndTime + (int)TickSpacing) <= (int)TickSpacing)
// lastTickHittable = false;
2017-04-18 13:24:16 +08:00
return converted ;
2017-03-17 13:39:38 +08:00
}
2018-04-13 17:19:50 +08:00
2020-09-17 16:40:05 +08:00
protected override IEnumerable < TaikoHitObject > ConvertHitObject ( HitObject obj , IBeatmap beatmap , CancellationToken cancellationToken )
2017-03-17 13:39:38 +08:00
{
2017-03-29 09:59:35 +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-11-12 18:16:51 +08:00
switch ( obj )
2017-03-17 13:39:38 +08:00
{
2023-11-29 16:30:21 +08:00
case IHasPath pathData :
2019-11-12 18:16:51 +08:00
{
2023-11-29 16:30:21 +08:00
if ( shouldConvertSliderToHits ( obj , beatmap , pathData , out int taikoDuration , out double tickSpacing ) )
2017-04-03 09:54:13 +08:00
{
2021-10-23 16:59:07 +08:00
IList < 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 )
2017-04-21 17:52:08 +08:00
{
2019-11-12 18:16:51 +08:00
IList < HitSampleInfo > currentSamples = allSamples [ i ] ;
2020-03-23 11:08:15 +08:00
yield return new Hit
2017-04-21 17:52:08 +08:00
{
2020-03-23 11:08:15 +08:00
StartTime = j ,
Samples = currentSamples ,
} ;
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 ;
2017-04-21 17:52:08 +08:00
}
2017-04-03 09:54:13 +08:00
}
2019-11-12 18:16:51 +08:00
else
2017-04-03 09:54:13 +08:00
{
2019-11-12 18:16:51 +08:00
yield return new DrumRoll
{
StartTime = obj . StartTime ,
Samples = obj . Samples ,
Duration = taikoDuration ,
} ;
}
break ;
2017-04-03 09:54:13 +08:00
}
2018-04-13 17:19:50 +08:00
2020-05-27 11:38:39 +08:00
case IHasDuration endTimeData :
2017-03-17 13:39:38 +08:00
{
2021-10-02 11:34:29 +08:00
double hitMultiplier = IBeatmapDifficultyInfo . DifficultyRange ( beatmap . Difficulty . 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
2017-04-03 09:54:13 +08:00
{
StartTime = obj . StartTime ,
2017-04-06 10:41:16 +08:00
Samples = obj . Samples ,
2019-11-12 18:16:51 +08:00
Duration = endTimeData . Duration ,
RequiredHits = ( int ) Math . Max ( 1 , endTimeData . Duration / 1000 * hitMultiplier )
2017-04-03 09:54:13 +08:00
} ;
2019-11-12 18:16:51 +08:00
break ;
2017-04-03 09:54:13 +08:00
}
2019-11-12 18:16:51 +08:00
default :
2017-04-03 09:54:13 +08:00
{
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 ,
2020-05-11 11:53:54 +08:00
Samples = samples ,
2020-03-23 11:08:15 +08:00
} ;
2019-11-12 18:16:51 +08:00
break ;
2017-04-03 09:54:13 +08:00
}
2017-03-30 14:51:16 +08:00
}
2017-03-11 23:34:21 +08:00
}
2018-05-07 09:51:30 +08:00
2023-11-29 16:30:21 +08:00
private bool shouldConvertSliderToHits ( HitObject obj , IBeatmap beatmap , IHasPath pathData , out int taikoDuration , out double tickSpacing )
2020-07-13 16:06:00 +08:00
{
// 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 ;
2023-11-29 16:30:21 +08:00
double distance = pathData . Path . ExpectedDistance . Value ? ? 0 ;
2023-11-28 20:14:56 +08:00
2023-11-29 15:41:19 +08:00
// Do not combine the following two lines!
2023-12-06 14:26:32 +08:00
distance * = VELOCITY_MULTIPLIER ;
2023-11-29 15:41:19 +08:00
distance * = spans ;
2020-07-13 16:06:00 +08:00
TimingControlPoint timingPoint = beatmap . ControlPointInfo . TimingPointAt ( obj . StartTime ) ;
double beatLength ;
2023-09-07 16:41:28 +08:00
if ( obj is IHasSliderVelocity hasSliderVelocity )
2023-09-15 17:13:04 +08:00
beatLength = LegacyRulesetExtensions . GetPrecisionAdjustedBeatLength ( hasSliderVelocity , timingPoint , TaikoRuleset . SHORT_NAME ) ;
2020-07-13 16:06:00 +08:00
else
2023-04-25 17:34:09 +08:00
beatLength = timingPoint . BeatLength ;
2020-07-13 16:06:00 +08:00
2023-12-06 21:00:35 +08:00
double sliderScoringPointDistance = osu_base_scoring_distance * ( beatmap . Difficulty . SliderMultiplier * VELOCITY_MULTIPLIER ) / beatmap . Difficulty . SliderTickRate ;
2020-07-13 16:06:00 +08:00
// The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
2021-10-02 11:34:29 +08:00
double taikoVelocity = sliderScoringPointDistance * beatmap . Difficulty . SliderTickRate ;
2021-01-12 16:50:22 +08:00
taikoDuration = ( int ) ( distance / taikoVelocity * beatLength ) ;
2020-07-13 16:06:00 +08:00
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
2021-10-02 11:34:29 +08:00
tickSpacing = Math . Min ( beatLength / beatmap . Difficulty . SliderTickRate , ( double ) taikoDuration / spans ) ;
2020-07-13 16:06:00 +08:00
return tickSpacing > 0
& & distance / osuVelocity * 1000 < 2 * beatLength ;
}
2018-05-07 09:51:30 +08:00
protected override Beatmap < TaikoHitObject > CreateBeatmap ( ) = > new TaikoBeatmap ( ) ;
2017-03-11 23:34:21 +08:00
}
}