osu/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs

233 lines
9.5 KiB
C#
Raw Normal View History

// 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 09:19:50 +00:00
2022-06-17 07:37:17 +00:00
#nullable disable
2018-04-13 09:19:50 +00: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;
using osu.Framework.Utils;
using System.Threading;
using JetBrains.Annotations;
2018-04-13 09:19:50 +00:00
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
2018-04-13 09:19:50 +00: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;
public TaikoBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
: base(beatmap, ruleset)
2018-04-13 09:19:50 +00:00
{
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
2018-04-13 09:19:50 +00:00
}
public override bool CanConvert() => true;
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
2018-04-13 09:19:50 +00:00
{
if (!(original.Difficulty is TaikoMultiplierAppliedDifficulty))
{
// Rewrite the beatmap info to add the slider velocity multiplier
original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty);
}
2018-04-13 09:19:50 +00:00
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
2018-04-13 09:19:50 +00:00
if (original.BeatmapInfo.Ruleset.OnlineID == 3)
2018-04-13 09:19:50 +00: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 20:46:02 +00:00
if (x.Skip(1).Any() && first is TaikoStrongableHitObject strong)
strong.IsStrong = true;
2018-04-13 09:19:50 +00:00
return first;
}).ToList();
}
return converted;
}
protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken)
2018-04-13 09:19:50 +00:00
{
// Old osu! used hit sounding to determine various hit type information
2019-11-08 05:04:57 +00:00
IList<HitSampleInfo> samples = obj.Samples;
2018-04-13 09:19:50 +00:00
2019-11-12 10:16:51 +00:00
switch (obj)
2018-04-13 09:19:50 +00:00
{
2019-11-12 10:16:51 +00:00
case IHasDistance distanceData:
{
if (shouldConvertSliderToHits(obj, beatmap, distanceData, out int taikoDuration, out double tickSpacing))
2018-04-13 09:19:50 +00:00
{
IList<IList<HitSampleInfo>> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(new[] { samples });
2018-04-13 09:19:50 +00:00
2019-11-12 10:16:51 +00:00
int i = 0;
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
2018-04-13 09:19:50 +00:00
{
2019-11-12 10:16:51 +00:00
IList<HitSampleInfo> currentSamples = allSamples[i];
yield return new Hit
2018-04-13 09:19:50 +00:00
{
StartTime = j,
Samples = currentSamples,
};
2019-11-12 10:16:51 +00:00
i = (i + 1) % allSamples.Count;
if (Precision.AlmostEquals(0, tickSpacing))
break;
2018-04-13 09:19:50 +00:00
}
}
2019-11-12 10:16:51 +00:00
else
2018-04-13 09:19:50 +00:00
{
2019-11-12 10:16:51 +00:00
yield return new DrumRoll
{
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = taikoDuration,
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4
2019-11-12 10:16:51 +00:00
};
}
break;
2018-04-13 09:19:50 +00:00
}
2020-05-27 03:38:39 +00:00
case IHasDuration endTimeData:
2018-04-13 09:19:50 +00:00
{
double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
2018-04-13 09:19:50 +00:00
2019-11-12 10:16:51 +00:00
yield return new Swell
2018-04-13 09:19:50 +00:00
{
StartTime = obj.StartTime,
Samples = obj.Samples,
2019-11-12 10:16:51 +00:00
Duration = endTimeData.Duration,
RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier)
2018-04-13 09:19:50 +00:00
};
2019-11-12 10:16:51 +00:00
break;
2018-04-13 09:19:50 +00:00
}
2019-11-12 10:16:51 +00:00
default:
2018-04-13 09:19:50 +00:00
{
yield return new Hit
2019-11-12 10:16:51 +00:00
{
StartTime = obj.StartTime,
Samples = samples,
};
2019-11-12 10:16:51 +00:00
break;
2018-04-13 09:19:50 +00:00
}
}
}
2018-05-07 01:51:30 +00:00
private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out int 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 * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
2021-08-31 07:31:48 +00:00
DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint;
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.SliderVelocity;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
// The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
double taikoVelocity = sliderScoringPointDistance * beatmap.Difficulty.SliderTickRate;
taikoDuration = (int)(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.Difficulty.SliderTickRate, (double)taikoDuration / spans);
return tickSpacing > 0
&& distance / osuVelocity * 1000 < 2 * beatLength;
}
2018-05-07 01:51:30 +00:00
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
// 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 12:23:24 +00:00
internal class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
{
public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
{
CopyFrom(difficulty);
}
[UsedImplicitly]
public TaikoMultiplierAppliedDifficulty()
{
}
#region Overrides of BeatmapDifficulty
public override BeatmapDifficulty Clone() => new TaikoMultiplierAppliedDifficulty(this);
public override void CopyTo(BeatmapDifficulty other)
{
base.CopyTo(other);
if (!(other is TaikoMultiplierAppliedDifficulty))
other.SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
public override void CopyFrom(IBeatmapDifficultyInfo other)
{
base.CopyFrom(other);
if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
#endregion
}
2018-04-13 09:19:50 +00:00
}
}