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 ;
2021-10-06 14:10:42 +08:00
using JetBrains.Annotations ;
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>
/// 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
{
2021-10-09 20:25:40 +08:00
if ( ! ( original . Difficulty is TaikoMultiplierAppliedDifficulty ) )
2021-08-30 14:30:17 +08:00
{
// Rewrite the beatmap info to add the slider velocity multiplier
2021-10-09 20:25:40 +08:00
original . Difficulty = new TaikoMultiplierAppliedDifficulty ( original . Difficulty ) ;
2021-08-30 14:30:17 +08:00
}
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
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 ;
double nextScrollSpeed = hasSliderVelocity . SliderVelocity ;
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 )
2018-04-13 17:19:50 +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 ;
2018-04-13 17:19:50 +08:00
return first ;
} ) . ToList ( ) ;
}
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;
2018-04-13 17:19:50 +08:00
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-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 :
{
2021-10-27 12:04:41 +08:00
if ( shouldConvertSliderToHits ( obj , beatmap , distanceData , out int taikoDuration , out double tickSpacing ) )
2018-04-13 17:19:50 +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 )
2018-04-13 17:19:50 +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
2018-04-13 17:19:50 +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 ;
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 ,
Duration = taikoDuration ,
} ;
}
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
{
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
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-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 ;
2018-04-13 17:19:50 +08:00
}
}
}
2018-05-07 09:51:30 +08:00
2021-01-12 16:50:22 +08:00
private bool shouldConvertSliderToHits ( HitObject obj , IBeatmap beatmap , IHasDistance distanceData , 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 ;
2021-08-30 13:40:25 +08:00
double distance = distanceData . Distance * spans * LegacyBeatmapEncoder . LEGACY_TAIKO_VELOCITY_MULTIPLIER ;
2020-07-13 16:06:00 +08:00
TimingControlPoint timingPoint = beatmap . ControlPointInfo . TimingPointAt ( obj . StartTime ) ;
double beatLength ;
2023-04-26 19:10:57 +08:00
if ( obj . LegacyBpmMultiplier . HasValue )
beatLength = timingPoint . BeatLength * obj . LegacyBpmMultiplier . Value ;
2023-04-25 17:34:09 +08:00
else if ( obj is IHasSliderVelocity hasSliderVelocity )
beatLength = timingPoint . BeatLength / hasSliderVelocity . SliderVelocity ;
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
2021-10-02 11:34:29 +08:00
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap . Difficulty . SliderMultiplier / 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 ( ) ;
2021-08-30 14:30:17 +08:00
2022-01-11 13:19:35 +08:00
// Important to note that this is subclassing a realm object.
// Realm doesn't allow this, but for now this can work since we aren't (in theory?) persisting this to the database.
// It is only used during beatmap conversion and processing.
2021-11-08 20:23:24 +08:00
internal class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
2021-08-30 14:30:17 +08:00
{
2021-10-09 20:25:40 +08:00
public TaikoMultiplierAppliedDifficulty ( IBeatmapDifficultyInfo difficulty )
2021-08-30 14:30:17 +08:00
{
2021-10-01 13:56:42 +08:00
CopyFrom ( difficulty ) ;
2021-08-30 14:30:17 +08:00
}
2021-10-06 14:10:42 +08:00
[UsedImplicitly]
2021-10-09 20:25:40 +08:00
public TaikoMultiplierAppliedDifficulty ( )
2021-10-06 14:10:42 +08:00
{
}
#region Overrides of BeatmapDifficulty
2021-10-01 13:56:42 +08:00
2022-01-13 12:23:41 +08:00
public override BeatmapDifficulty Clone ( ) = > new TaikoMultiplierAppliedDifficulty ( this ) ;
2021-10-06 14:10:42 +08:00
public override void CopyTo ( BeatmapDifficulty other )
{
base . CopyTo ( other ) ;
2021-10-09 20:25:40 +08:00
if ( ! ( other is TaikoMultiplierAppliedDifficulty ) )
2021-11-08 20:23:54 +08:00
other . SliderMultiplier / = LegacyBeatmapEncoder . LEGACY_TAIKO_VELOCITY_MULTIPLIER ;
2021-08-30 14:30:17 +08:00
}
2021-10-06 14:10:42 +08:00
public override void CopyFrom ( IBeatmapDifficultyInfo other )
{
base . CopyFrom ( other ) ;
2021-10-09 20:25:40 +08:00
if ( ! ( other is TaikoMultiplierAppliedDifficulty ) )
2021-10-06 14:10:42 +08:00
SliderMultiplier * = LegacyBeatmapEncoder . LEGACY_TAIKO_VELOCITY_MULTIPLIER ;
}
#endregion
2021-08-30 14:30:17 +08:00
}
2018-04-13 17:19:50 +08:00
}
}