2019-01-24 17:43:03 +09: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 18:19:50 +09:00
2017-03-12 00:34:21 +09:00
using osu.Game.Beatmaps ;
2017-04-18 16:05:58 +09:00
using osu.Game.Rulesets.Objects ;
using osu.Game.Rulesets.Objects.Types ;
using osu.Game.Rulesets.Taiko.Objects ;
2017-04-03 10:54:13 +09:00
using System ;
2017-03-12 00:34:21 +09:00
using System.Collections.Generic ;
2017-03-17 14:39:38 +09:00
using System.Linq ;
2020-09-17 16:30:34 +09:00
using osu.Framework.Utils ;
2020-09-17 17:40:05 +09:00
using System.Threading ;
2017-04-06 11:41:16 +09:00
using osu.Game.Audio ;
2017-05-23 13:55:18 +09:00
using osu.Game.Beatmaps.ControlPoints ;
2023-09-15 17:38:34 +09:00
using osu.Game.Rulesets.Objects.Legacy ;
2018-04-13 18:19:50 +09:00
2017-04-18 16:05:58 +09:00
namespace osu.Game.Rulesets.Taiko.Beatmaps
2017-03-12 00:34:21 +09:00
{
2017-04-18 09:38:52 +09:00
internal class TaikoBeatmapConverter : BeatmapConverter < TaikoHitObject >
2017-03-12 00:34:21 +09:00
{
2023-12-06 15:26:32 +09: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 20:32:03 +09: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 18:19:50 +09:00
2017-04-03 20:32:03 +09:00
/// <summary>
/// Base osu! slider scoring distance.
/// </summary>
private const float osu_base_scoring_distance = 100 ;
2018-04-13 18:19:50 +09:00
2017-08-22 14:21:28 +09:00
private readonly bool isForCurrentRuleset ;
2018-04-13 18:19:50 +09:00
2019-12-24 16:02:16 +09:00
public TaikoBeatmapConverter ( IBeatmap beatmap , Ruleset ruleset )
: base ( beatmap , ruleset )
2017-03-12 00:34:21 +09:00
{
2019-12-24 16:02:16 +09:00
isForCurrentRuleset = beatmap . BeatmapInfo . Ruleset . Equals ( ruleset . RulesetInfo ) ;
2017-08-22 14:18:17 +09:00
}
2018-04-13 18:19:50 +09:00
2019-12-23 17:44:18 +09:00
public override bool CanConvert ( ) = > true ;
2020-09-17 17:40:05 +09:00
protected override Beatmap < TaikoHitObject > ConvertBeatmap ( IBeatmap original , CancellationToken cancellationToken )
2017-08-22 14:18:17 +09:00
{
2020-09-17 17:40:05 +09:00
Beatmap < TaikoHitObject > converted = base . ConvertBeatmap ( original , cancellationToken ) ;
2018-04-13 18:19:50 +09:00
2022-10-27 07:25:50 +02: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 17:55:38 +02:00
if ( hitObject is not IHasSliderVelocity hasSliderVelocity ) continue ;
2023-09-06 18:59:15 +09:00
double nextScrollSpeed = hasSliderVelocity . SliderVelocityMultiplier ;
2022-10-27 23:41:17 +02:00
EffectControlPoint currentEffectPoint = converted . ControlPointInfo . EffectPointAt ( hitObject . StartTime ) ;
2022-10-27 07:25:50 +02:00
2022-10-27 23:41:17 +02:00
if ( ! Precision . AlmostEquals ( lastScrollSpeed , nextScrollSpeed , acceptableDifference : currentEffectPoint . ScrollSpeedBindable . Precision ) )
2022-10-27 07:25:50 +02:00
{
2022-10-27 23:41:17 +02:00
converted . ControlPointInfo . Add ( hitObject . StartTime , new EffectControlPoint
{
KiaiMode = currentEffectPoint . KiaiMode ,
ScrollSpeed = lastScrollSpeed = nextScrollSpeed ,
} ) ;
2022-10-27 07:25:50 +02:00
}
}
}
2022-01-27 15:19:48 +09:00
if ( original . BeatmapInfo . Ruleset . OnlineID = = 3 )
2017-03-12 00:34:21 +09:00
{
2017-10-01 00:15:23 +02: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-14 21:46:02 +01:00
if ( x . Skip ( 1 ) . Any ( ) & & first is TaikoStrongableHitObject strong )
2020-12-13 12:36:39 +01:00
strong . IsStrong = true ;
2017-10-01 00:15:23 +02:00
return first ;
} ) . ToList ( ) ;
}
2018-04-13 18:19:50 +09:00
2023-06-06 16:59:28 +09: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 14:24:16 +09:00
return converted ;
2017-03-17 14:39:38 +09:00
}
2018-04-13 18:19:50 +09:00
2020-09-17 17:40:05 +09:00
protected override IEnumerable < TaikoHitObject > ConvertHitObject ( HitObject obj , IBeatmap beatmap , CancellationToken cancellationToken )
2017-03-17 14:39:38 +09:00
{
2017-03-29 10:59:35 +09:00
// Old osu! used hit sounding to determine various hit type information
2019-11-08 14:04:57 +09:00
IList < HitSampleInfo > samples = obj . Samples ;
2018-04-13 18:19:50 +09:00
2019-11-12 18:16:51 +08:00
switch ( obj )
2017-03-17 14:39:38 +09:00
{
2023-11-29 17:30:21 +09:00
case IHasPath pathData :
2019-11-12 18:16:51 +08:00
{
2023-11-29 17:30:21 +09:00
if ( shouldConvertSliderToHits ( obj , beatmap , pathData , out int taikoDuration , out double tickSpacing ) )
2017-04-03 10:54:13 +09:00
{
2021-10-23 01:59:07 -07:00
IList < IList < HitSampleInfo > > allSamples = obj is IHasPathWithRepeats curveData ? curveData . NodeSamples : new List < IList < HitSampleInfo > > ( new [ ] { samples } ) ;
2018-04-13 18:19:50 +09: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 18:52:08 +09:00
{
2019-11-12 18:16:51 +08:00
IList < HitSampleInfo > currentSamples = allSamples [ i ] ;
2020-03-23 12:08:15 +09:00
yield return new Hit
2017-04-21 18:52:08 +09:00
{
2020-03-23 12:08:15 +09:00
StartTime = j ,
Samples = currentSamples ,
} ;
2019-11-12 18:16:51 +08:00
i = ( i + 1 ) % allSamples . Count ;
2020-09-18 13:06:41 +09:00
if ( Precision . AlmostEquals ( 0 , tickSpacing ) )
break ;
2017-04-21 18:52:08 +09:00
}
2017-04-03 10:54:13 +09:00
}
2019-11-12 18:16:51 +08:00
else
2017-04-03 10:54:13 +09: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 10:54:13 +09:00
}
2018-04-13 18:19:50 +09:00
2020-05-27 12:38:39 +09:00
case IHasDuration endTimeData :
2017-03-17 14:39:38 +09:00
{
2021-10-02 12:34:29 +09:00
double hitMultiplier = IBeatmapDifficultyInfo . DifficultyRange ( beatmap . Difficulty . OverallDifficulty , 3 , 5 , 7.5 ) * swell_hit_multiplier ;
2018-04-13 18:19:50 +09:00
2019-11-12 18:16:51 +08:00
yield return new Swell
2017-04-03 10:54:13 +09:00
{
StartTime = obj . StartTime ,
2017-04-06 11:41:16 +09: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 10:54:13 +09:00
} ;
2019-11-12 18:16:51 +08:00
break ;
2017-04-03 10:54:13 +09:00
}
2019-11-12 18:16:51 +08:00
default :
2017-04-03 10:54:13 +09:00
{
2020-03-23 12:08:15 +09:00
yield return new Hit
2019-11-12 18:16:51 +08:00
{
2020-03-23 12:08:15 +09:00
StartTime = obj . StartTime ,
2020-05-11 12:53:54 +09:00
Samples = samples ,
2020-03-23 12:08:15 +09:00
} ;
2019-11-12 18:16:51 +08:00
break ;
2017-04-03 10:54:13 +09:00
}
2017-03-30 15:51:16 +09:00
}
2017-03-12 00:34:21 +09:00
}
2018-05-07 10:51:30 +09:00
2023-11-29 17:30:21 +09:00
private bool shouldConvertSliderToHits ( HitObject obj , IBeatmap beatmap , IHasPath pathData , out int taikoDuration , out double tickSpacing )
2020-07-13 17:06:00 +09: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 17:30:21 +09:00
double distance = pathData . Path . ExpectedDistance . Value ? ? 0 ;
2023-11-28 21:14:56 +09:00
2023-11-29 16:41:19 +09:00
// Do not combine the following two lines!
2023-12-06 15:26:32 +09:00
distance * = VELOCITY_MULTIPLIER ;
2023-11-29 16:41:19 +09:00
distance * = spans ;
2020-07-13 17:06:00 +09:00
TimingControlPoint timingPoint = beatmap . ControlPointInfo . TimingPointAt ( obj . StartTime ) ;
double beatLength ;
2023-09-07 17:41:28 +09:00
if ( obj is IHasSliderVelocity hasSliderVelocity )
2023-09-15 18:13:04 +09:00
beatLength = LegacyRulesetExtensions . GetPrecisionAdjustedBeatLength ( hasSliderVelocity , timingPoint , TaikoRuleset . SHORT_NAME ) ;
2020-07-13 17:06:00 +09:00
else
2023-04-25 11:34:09 +02:00
beatLength = timingPoint . BeatLength ;
2020-07-13 17:06:00 +09:00
2023-12-06 22:00:35 +09:00
double sliderScoringPointDistance = osu_base_scoring_distance * ( beatmap . Difficulty . SliderMultiplier * VELOCITY_MULTIPLIER ) / beatmap . Difficulty . SliderTickRate ;
2020-07-13 17:06:00 +09:00
// The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
2021-10-02 12:34:29 +09:00
double taikoVelocity = sliderScoringPointDistance * beatmap . Difficulty . SliderTickRate ;
2021-01-12 17:50:22 +09:00
taikoDuration = ( int ) ( distance / taikoVelocity * beatLength ) ;
2020-07-13 17:06:00 +09: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 12:34:29 +09:00
tickSpacing = Math . Min ( beatLength / beatmap . Difficulty . SliderTickRate , ( double ) taikoDuration / spans ) ;
2020-07-13 17:06:00 +09:00
return tickSpacing > 0
& & distance / osuVelocity * 1000 < 2 * beatLength ;
}
2018-05-07 10:51:30 +09:00
protected override Beatmap < TaikoHitObject > CreateBeatmap ( ) = > new TaikoBeatmap ( ) ;
2017-03-12 00:34:21 +09:00
}
}