mirror of
https://github.com/osukey/osukey.git
synced 2025-05-30 01:47:30 +09:00
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:
commit
5e5b7a4b36
@ -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(),
|
||||||
|
@ -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(),
|
||||||
|
@ -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(),
|
||||||
|
@ -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(),
|
||||||
|
65
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
Normal file
65
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
|
@ -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>
|
||||||
{
|
{
|
||||||
|
@ -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(),
|
||||||
|
@ -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(),
|
||||||
|
@ -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()
|
||||||
|
20
osu.Game/Rulesets/Mods/IApplicableToRate.cs
Normal file
20
osu.Game/Rulesets/Mods/IApplicableToRate.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user