Merge pull request #11697 from bdach/autoplay-rate-independence

Adjust osu! autoplay generation to ensure constant SPM with rate-changing mods
This commit is contained in:
Dean Herbert 2021-02-11 18:30:25 +09:00 committed by GitHub
commit 5e5b7a4b36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 209 additions and 58 deletions

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Replays;
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModAutoplay : ModAutoplay<CatchHitObject> public class CatchModAutoplay : ModAutoplay<CatchHitObject>
{ {
public override Score CreateReplayScore(IBeatmap beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(), Replay = new CatchAutoGenerator(beatmap).Generate(),

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Replays;
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModCinema : ModCinema<CatchHitObject> public class CatchModCinema : ModCinema<CatchHitObject>
{ {
public override Score CreateReplayScore(IBeatmap beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(), Replay = new CatchAutoGenerator(beatmap).Generate(),

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModAutoplay : ModAutoplay<ManiaHitObject> public class ManiaModAutoplay : ModAutoplay<ManiaHitObject>
{ {
public override Score CreateReplayScore(IBeatmap beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModCinema : ModCinema<ManiaHitObject> public class ManiaModCinema : ModCinema<ManiaHitObject>
{ {
public override Score CreateReplayScore(IBeatmap beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),

View File

@ -0,0 +1,65 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModAutoplay : OsuModTestScene
{
[Test]
public void TestSpmUnaffectedByRateAdjust()
=> runSpmTest(new OsuModDaycore
{
SpeedChange = { Value = 0.88 }
});
[Test]
public void TestSpmUnaffectedByTimeRamp()
=> runSpmTest(new ModWindUp
{
InitialRate = { Value = 0.7 },
FinalRate = { Value = 1.3 }
});
private void runSpmTest(Mod mod)
{
SpinnerSpmCounter spmCounter = null;
CreateModTest(new ModTestData
{
Autoplay = true,
Mod = mod,
Beatmap = new Beatmap
{
HitObjects =
{
new Spinner
{
Duration = 2000,
Position = OsuPlayfield.BASE_SIZE / 2
}
}
},
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
});
AddUntilStep("fetch SPM counter", () =>
{
spmCounter = this.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
return spmCounter != null;
});
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
}
}
}

View File

@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -65,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestAutoMod : OsuModAutoplay private class TestAutoMod : OsuModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
Replay = new MissingAutoGenerator(beatmap).Generate() Replay = new MissingAutoGenerator(beatmap, mods).Generate()
}; };
} }
@ -76,8 +78,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap; public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap;
public MissingAutoGenerator(IBeatmap beatmap) public MissingAutoGenerator(IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(beatmap) : base(beatmap, mods)
{ {
} }

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
inputManager.AllowUserCursorMovement = false; inputManager.AllowUserCursorMovement = false;
// Generate the replay frames the cursor should follow // Generate the replay frames the cursor should follow
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap).Generate().Frames.Cast<OsuReplayFrame>().ToList(); replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast<OsuReplayFrame>().ToList();
} }
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -16,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap).Generate() Replay = new OsuAutoGenerator(beatmap, mods).Generate()
}; };
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -16,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap).Generate() Replay = new OsuAutoGenerator(beatmap, mods).Generate()
}; };
} }
} }

View File

@ -6,10 +6,12 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
@ -33,11 +35,6 @@ namespace osu.Game.Rulesets.Osu.Replays
#region Constants #region Constants
/// <summary>
/// The "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
/// </summary>
private readonly double reactionTime;
private readonly HitWindows defaultHitWindows; private readonly HitWindows defaultHitWindows;
/// <summary> /// <summary>
@ -49,12 +46,9 @@ namespace osu.Game.Rulesets.Osu.Replays
#region Construction / Initialisation #region Construction / Initialisation
public OsuAutoGenerator(IBeatmap beatmap) public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(beatmap) : base(beatmap, mods)
{ {
// Already superhuman, but still somewhat realistic
reactionTime = ApplyModsToRate(100);
defaultHitWindows = new OsuHitWindows(); defaultHitWindows = new OsuHitWindows();
defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
} }
@ -240,7 +234,7 @@ namespace osu.Game.Rulesets.Osu.Replays
OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[^1]; OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[^1];
// Wait until Auto could "see and react" to the next note. // Wait until Auto could "see and react" to the next note.
double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime); double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt));
if (waitTime > lastFrame.Time) if (waitTime > lastFrame.Time)
{ {
@ -250,7 +244,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Vector2 lastPosition = lastFrame.Position; Vector2 lastPosition = lastFrame.Position;
double timeDifference = ApplyModsToTime(h.StartTime - lastFrame.Time); double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
// Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
if (timeDifference > 0 && // Sanity checks if (timeDifference > 0 && // Sanity checks
@ -258,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.Replays
timeDifference >= 266)) // ... or the beats are slow enough to tap anyway. timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
{ {
// Perform eased movement // Perform eased movement
for (double time = lastFrame.Time + FrameDelay; time < h.StartTime; time += FrameDelay) for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time))
{ {
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing); Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing);
AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions }); AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions });
@ -272,6 +266,14 @@ namespace osu.Game.Rulesets.Osu.Replays
} }
} }
/// <summary>
/// Calculates the "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
/// </summary>
/// <remarks>
/// Already superhuman, but still somewhat realistic.
/// </remarks>
private double getReactionTime(double timeInstant) => ApplyModsToRate(timeInstant, 100);
// Add frames to click the hitobject // Add frames to click the hitobject
private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection) private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection)
{ {
@ -341,17 +343,23 @@ namespace osu.Game.Rulesets.Osu.Replays
float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X); float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X);
double t; double t;
double previousFrame = h.StartTime;
for (double j = h.StartTime + FrameDelay; j < spinner.EndTime; j += FrameDelay) for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame))
{ {
t = ApplyModsToTime(j - h.StartTime) * spinnerDirection; t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection;
angle += (float)t / 20;
Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame((int)j, new Vector2(pos.X, pos.Y), action)); AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action));
previousFrame = nextFrame;
} }
t = ApplyModsToTime(spinner.EndTime - h.StartTime) * spinnerDirection; t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection;
Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); angle += (float)t / 20;
Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action)); AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action));
@ -359,7 +367,7 @@ namespace osu.Game.Rulesets.Osu.Replays
break; break;
case Slider slider: case Slider slider:
for (double j = FrameDelay; j < slider.Duration; j += FrameDelay) for (double j = GetFrameDelay(slider.StartTime); j < slider.Duration; j += GetFrameDelay(slider.StartTime + j))
{ {
Vector2 pos = slider.StackedPositionAt(j / slider.Duration); Vector2 pos = slider.StackedPositionAt(j / slider.Duration);
AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action)); AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action));

View File

@ -5,7 +5,9 @@ using osuTK;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
@ -22,33 +24,61 @@ namespace osu.Game.Rulesets.Osu.Replays
public const float SPIN_RADIUS = 50; public const float SPIN_RADIUS = 50;
/// <summary>
/// The time in ms between each ReplayFrame.
/// </summary>
protected readonly double FrameDelay;
#endregion #endregion
#region Construction / Initialisation #region Construction / Initialisation
protected Replay Replay; protected Replay Replay;
protected List<ReplayFrame> Frames => Replay.Frames; protected List<ReplayFrame> Frames => Replay.Frames;
private readonly IReadOnlyList<IApplicableToRate> timeAffectingMods;
protected OsuAutoGeneratorBase(IBeatmap beatmap) protected OsuAutoGeneratorBase(IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay(); Replay = new Replay();
// We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps. timeAffectingMods = mods.OfType<IApplicableToRate>().ToList();
FrameDelay = ApplyModsToRate(1000.0 / 60.0);
} }
#endregion #endregion
#region Utilities #region Utilities
protected double ApplyModsToTime(double v) => v; /// <summary>
protected double ApplyModsToRate(double v) => v; /// Returns the real duration of time between <paramref name="startTime"/> and <paramref name="endTime"/>
/// after applying rate-affecting mods.
/// </summary>
/// <remarks>
/// This method should only be used when <paramref name="startTime"/> and <paramref name="endTime"/> are very close.
/// That is because the track rate might be changing with time,
/// and the method used here is a rough instantaneous approximation.
/// </remarks>
/// <param name="startTime">The start time of the time delta, in original track time.</param>
/// <param name="endTime">The end time of the time delta, in original track time.</param>
protected double ApplyModsToTimeDelta(double startTime, double endTime)
{
double delta = endTime - startTime;
foreach (var mod in timeAffectingMods)
delta /= mod.ApplyToRate(startTime);
return delta;
}
protected double ApplyModsToRate(double time, double rate)
{
foreach (var mod in timeAffectingMods)
rate = mod.ApplyToRate(time, rate);
return rate;
}
/// <summary>
/// Calculates the interval after which the next <see cref="ReplayFrame"/> should be generated,
/// in milliseconds.
/// </summary>
/// <param name="time">The time of the previous frame.</param>
protected double GetFrameDelay(double time)
=> ApplyModsToRate(time, 1000.0 / 60);
private class ReplayFrameComparer : IComparer<ReplayFrame> private class ReplayFrameComparer : IComparer<ReplayFrame>
{ {

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModAutoplay : ModAutoplay<TaikoHitObject> public class TaikoModAutoplay : ModAutoplay<TaikoHitObject>
{ {
public override Score CreateReplayScore(IBeatmap beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(), Replay = new TaikoAutoGenerator(beatmap).Generate(),

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModCinema : ModCinema<TaikoHitObject> public class TaikoModCinema : ModCinema<TaikoHitObject>
{ {
public override Score CreateReplayScore(IBeatmap beatmap) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {
ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } }, ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(), Replay = new TaikoAutoGenerator(beatmap).Generate(),

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty<Mod>()); var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty<Mod>());
return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap)); return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap, Array.Empty<Mod>()));
} }
protected override void AddCheckSteps() protected override void AddCheckSteps()

View File

@ -0,0 +1,20 @@
// 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.
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// Interface that should be implemented by mods that affect the track playback speed,
/// and in turn, values of the track rate.
/// </summary>
public interface IApplicableToRate : IApplicableToAudio
{
/// <summary>
/// Returns the playback rate at <paramref name="time"/> after this mod is applied.
/// </summary>
/// <param name="time">The time instant at which the playback rate is queried.</param>
/// <param name="rate">The playback rate before applying this mod.</param>
/// <returns>The playback rate after applying this mod.</returns>
double ApplyToRate(double time, double rate = 1);
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -15,7 +16,10 @@ namespace osu.Game.Rulesets.Mods
public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToDrawableRuleset<T> public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToDrawableRuleset<T>
where T : HitObject where T : HitObject
{ {
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset) => drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap)); public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
{
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
}
} }
public abstract class ModAutoplay : Mod, IApplicableFailOverride public abstract class ModAutoplay : Mod, IApplicableFailOverride
@ -35,6 +39,11 @@ namespace osu.Game.Rulesets.Mods
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
[Obsolete("Use the mod-supporting override")] // can be removed 20210731
public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() }; public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() };
#pragma warning disable 618
public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => CreateReplayScore(beatmap);
#pragma warning restore 618
} }
} }

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
{ {
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset) public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
{ {
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap)); drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
// AlwaysPresent required for hitsounds // AlwaysPresent required for hitsounds
drawableRuleset.Playfield.AlwaysPresent = true; drawableRuleset.Playfield.AlwaysPresent = true;

View File

@ -8,7 +8,7 @@ using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModRateAdjust : Mod, IApplicableToAudio public abstract class ModRateAdjust : Mod, IApplicableToRate
{ {
public abstract BindableNumber<double> SpeedChange { get; } public abstract BindableNumber<double> SpeedChange { get; }
@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mods
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
} }
public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value;
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
} }
} }

View File

@ -14,7 +14,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToAudio public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToRate
{ {
/// <summary> /// <summary>
/// The point in the beatmap at which the final ramping rate should be reached. /// The point in the beatmap at which the final ramping rate should be reached.
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mods
protected ModTimeRamp() protected ModTimeRamp()
{ {
// for preview purpose at song select. eventually we'll want to be able to update every frame. // for preview purpose at song select. eventually we'll want to be able to update every frame.
FinalRate.BindValueChanged(val => applyRateAdjustment(1), true); FinalRate.BindValueChanged(val => applyRateAdjustment(double.PositiveInfinity), true);
AdjustPitch.BindValueChanged(applyPitchAdjustment); AdjustPitch.BindValueChanged(applyPitchAdjustment);
} }
@ -75,17 +75,24 @@ namespace osu.Game.Rulesets.Mods
finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart); finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart);
} }
public double ApplyToRate(double time, double rate = 1)
{
double amount = (time - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime);
double ramp = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1);
// round the end result to match the bindable SpeedChange's precision, in case this is called externally.
return rate * Math.Round(ramp, 2);
}
public virtual void Update(Playfield playfield) public virtual void Update(Playfield playfield)
{ {
applyRateAdjustment((track.CurrentTime - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime)); applyRateAdjustment(track.CurrentTime);
} }
/// <summary> /// <summary>
/// Adjust the rate along the specified ramp /// Adjust the rate along the specified ramp.
/// </summary> /// </summary>
/// <param name="amount">The amount of adjustment to apply (from 0..1).</param> private void applyRateAdjustment(double time) => SpeedChange.Value = ApplyToRate(time);
private void applyRateAdjustment(double amount) =>
SpeedChange.Value = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1);
private void applyPitchAdjustment(ValueChangedEvent<bool> adjustPitchSetting) private void applyPitchAdjustment(ValueChangedEvent<bool> adjustPitchSetting)
{ {

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.UI
protected IRulesetConfigManager Config { get; private set; } protected IRulesetConfigManager Config { get; private set; }
[Cached(typeof(IReadOnlyList<Mod>))] [Cached(typeof(IReadOnlyList<Mod>))]
protected override IReadOnlyList<Mod> Mods { get; } public sealed override IReadOnlyList<Mod> Mods { get; }
private FrameStabilityContainer frameStabilityContainer; private FrameStabilityContainer frameStabilityContainer;
@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.UI
/// <summary> /// <summary>
/// The mods which are to be applied. /// The mods which are to be applied.
/// </summary> /// </summary>
protected abstract IReadOnlyList<Mod> Mods { get; } public abstract IReadOnlyList<Mod> Mods { get; }
/// <summary>~ /// <summary>~
/// The associated ruleset. /// The associated ruleset.