1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-13 16:13:34 +08:00

Merge pull request #25689 from peppy/taiko-multiplier-fix

Fix osu!taiko slider velocity being written incorrectly to `.osu` file on export
This commit is contained in:
Bartłomiej Dach 2023-12-07 14:12:42 +01:00 committed by GitHub
commit 07da9d95a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 70 additions and 68 deletions

View File

@ -1,10 +1,16 @@
// 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.
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Utils;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Beatmaps;
using osu.Game.Tests.Visual;
using SharpCompress.Archives.Zip;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
@ -12,6 +18,44 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
protected override Ruleset CreateRuleset() => new TaikoRuleset();
[TestCase(null)]
[TestCase(1f)]
[TestCase(2f)]
[TestCase(2.4f)]
public void TestTaikoSliderMultiplierInExport(float? multiplier)
{
if (multiplier.HasValue)
AddStep("Set slider multiplier", () => EditorBeatmap.Difficulty.SliderMultiplier = multiplier.Value);
SaveEditor();
AddStep("export beatmap", () => Game.BeatmapManager.Export(EditorBeatmap.BeatmapInfo.BeatmapSet!).WaitSafely());
AddAssert("check slider multiplier correct in file", () =>
{
string export = LocalStorage.GetFiles("exports").First();
using (var stream = LocalStorage.GetStream(export))
using (var zip = ZipArchive.Open(stream))
{
using (var osuStream = zip.Entries.First().OpenEntryStream())
using (var reader = new StreamReader(osuStream))
{
string? line;
while ((line = reader.ReadLine()) != null)
{
if (line.StartsWith("SliderMultiplier", StringComparison.Ordinal))
{
return float.Parse(line.Split(':', StringSplitOptions.TrimEntries).Last(), provider: CultureInfo.InvariantCulture);
}
}
}
}
return 0;
}, () => Is.EqualTo(multiplier ?? new BeatmapDifficulty().SliderMultiplier).Within(Precision.FLOAT_EPSILON));
}
[Test]
public void TestTaikoSliderMultiplier()
{
@ -27,11 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor
bool assertTaikoSliderMulitplier()
{
// we can only assert value correctness on TaikoMultiplierAppliedDifficulty, because that is the final difficulty converted taiko beatmaps use.
// therefore, ensure that we have that difficulty type by calling .CopyFrom(), which is a no-op if the type is already correct.
var taikoDifficulty = new TaikoBeatmapConverter.TaikoMultiplierAppliedDifficulty();
taikoDifficulty.CopyFrom(EditorBeatmap.Difficulty);
return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2);
return Precision.AlmostEquals(EditorBeatmap.Difficulty.SliderMultiplier, 2);
}
}
}

View File

@ -10,16 +10,25 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Utils;
using System.Threading;
using JetBrains.Annotations;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Objects.Legacy;
namespace osu.Game.Rulesets.Taiko.Beatmaps
{
internal class TaikoBeatmapConverter : BeatmapConverter<TaikoHitObject>
{
/// <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;
/// <summary>
/// Because swells are easier in taiko than spinners are in osu!,
/// legacy taiko multiplies a factor when converting the number of required hits.
@ -43,12 +52,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
if (!(original.Difficulty is TaikoMultiplierAppliedDifficulty))
{
// Rewrite the beatmap info to add the slider velocity multiplier
original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty);
}
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
if (original.BeatmapInfo.Ruleset.OnlineID == 0)
@ -180,7 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = pathData.Path.ExpectedDistance.Value ?? 0;
// Do not combine the following two lines!
distance *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
distance *= VELOCITY_MULTIPLIER;
distance *= spans;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
@ -192,7 +195,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
else
beatLength = timingPoint.BeatLength;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
double sliderScoringPointDistance = osu_base_scoring_distance * (beatmap.Difficulty.SliderMultiplier * VELOCITY_MULTIPLIER) / 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;
@ -218,41 +221,5 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}
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.
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
}
}
}

View File

@ -1,14 +1,14 @@
// 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.
using osu.Game.Rulesets.Objects.Types;
using System.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
EffectControlPoint effectPoint = controlPointInfo.EffectPointAt(StartTime);
double scoringDistance = base_distance * difficulty.SliderMultiplier * effectPoint.ScrollSpeed;
double scoringDistance = base_distance * (difficulty.SliderMultiplier * TaikoBeatmapConverter.VELOCITY_MULTIPLIER) * effectPoint.ScrollSpeed;
Velocity = scoringDistance / timingPoint.BeatLength;
TickRate = difficulty.SliderTickRate == 3 ? 3 : 4;
@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
double IHasDistance.Distance => Duration * Velocity;
SliderPath IHasPath.Path
=> new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER);
=> new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.VELOCITY_MULTIPLIER);
#endregion
}

View File

@ -16,6 +16,7 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Rulesets.Timing;
@ -77,7 +78,11 @@ namespace osu.Game.Rulesets.Taiko.UI
// We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
return (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
// Stable internally increased the slider velocity of objects by a factor of `VELOCITY_MULTIPLIER`.
// To simulate this, we shrink the time range by that factor here.
// This, when combined with the rest of the scrolling ruleset machinery (see `MultiplierControlPoint` et al.),
// has the effect of increasing each multiplier control point's multiplier by `VELOCITY_MULTIPLIER`, ensuring parity with stable.
return (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate / TaikoBeatmapConverter.VELOCITY_MULTIPLIER;
}
protected override void UpdateAfterChildren()

View File

@ -23,12 +23,6 @@ namespace osu.Game.Beatmaps.Formats
{
public const int FIRST_LAZER_VERSION = 128;
/// <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>
public const float LEGACY_TAIKO_VELOCITY_MULTIPLIER = 1.4f;
private readonly IBeatmap beatmap;
private readonly ISkin? skin;
@ -149,11 +143,7 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.Difficulty.OverallDifficulty}"));
writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.Difficulty.ApproachRate}"));
// Taiko adjusts the slider multiplier (see: LEGACY_TAIKO_VELOCITY_MULTIPLIER)
writer.WriteLine(onlineRulesetID == 1
? FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}")
: FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier}"));
writer.WriteLine(FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier}"));
writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.Difficulty.SliderTickRate}"));
}