1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-23 03:02:55 +08:00

Merge pull request #17429 from peppy/fix-old-legacy-replays

Fix replays set on old beatmaps having incorrect timing
This commit is contained in:
Bartłomiej Dach 2022-03-24 23:45:42 +01:00 committed by GitHub
commit e58b742728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 100 additions and 16 deletions

View File

@ -8,6 +8,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
@ -64,6 +65,62 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[TestCase(3, true)]
[TestCase(6, false)]
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied)
{
const double first_frame_time = 48;
const double second_frame_time = 65;
var decoder = new TestLegacyScoreDecoder(beatmapVersion);
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
{
var score = decoder.Parse(resourceStream);
Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
}
}
[TestCase(3)]
[TestCase(6)]
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION)]
public void TestLegacyBeatmapReplayOffsetsEncodeDecode(int beatmapVersion)
{
const double first_frame_time = 2000;
const double second_frame_time = 3000;
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset)
{
BeatmapInfo =
{
BeatmapVersion = beatmapVersion
}
};
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(first_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
new OsuReplayFrame(second_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};
var decodedAfterEncode = encodeThenDecode(beatmapVersion, score, beatmap);
Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(first_frame_time));
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time));
}
[Test] [Test]
public void TestCultureInvariance() public void TestCultureInvariance()
{ {
@ -86,15 +143,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// rather than the classic ASCII U+002D HYPHEN-MINUS. // rather than the classic ASCII U+002D HYPHEN-MINUS.
CultureInfo.CurrentCulture = new CultureInfo("se"); CultureInfo.CurrentCulture = new CultureInfo("se");
var encodeStream = new MemoryStream(); var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
var encoder = new LegacyScoreEncoder(score, beatmap);
encoder.Encode(encodeStream);
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
var decoder = new TestLegacyScoreDecoder();
var decodedAfterEncode = decoder.Parse(decodeStream);
Assert.Multiple(() => Assert.Multiple(() =>
{ {
@ -110,6 +159,20 @@ namespace osu.Game.Tests.Beatmaps.Formats
}); });
} }
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();
var encoder = new LegacyScoreEncoder(score, beatmap);
encoder.Encode(encodeStream);
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
var decoder = new TestLegacyScoreDecoder(beatmapVersion);
var decodedAfterEncode = decoder.Parse(decodeStream);
return decodedAfterEncode;
}
[TearDown] [TearDown]
public void TearDown() public void TearDown()
{ {
@ -118,6 +181,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacyScoreDecoder : LegacyScoreDecoder private class TestLegacyScoreDecoder : LegacyScoreDecoder
{ {
private readonly int beatmapVersion;
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[] private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
{ {
new OsuRuleset(), new OsuRuleset(),
@ -126,6 +191,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
new ManiaRuleset() new ManiaRuleset()
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID); }.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
public TestLegacyScoreDecoder(int beatmapVersion = LegacyBeatmapDecoder.LATEST_VERSION)
{
this.beatmapVersion = beatmapVersion;
}
protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId]; protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
@ -134,7 +204,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
MD5Hash = md5Hash, MD5Hash = md5Hash,
Ruleset = new OsuRuleset().RulesetInfo, Ruleset = new OsuRuleset().RulesetInfo,
Difficulty = new BeatmapDifficulty() Difficulty = new BeatmapDifficulty(),
BeatmapVersion = beatmapVersion,
} }
}); });
} }

View File

@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Formats
{ {
public class LegacyBeatmapDecoder : LegacyDecoder<Beatmap> public class LegacyBeatmapDecoder : LegacyDecoder<Beatmap>
{ {
/// <summary>
/// An offset which needs to be applied to old beatmaps (v4 and lower) to correct timing changes that were applied at a game client level.
/// </summary>
public const int EARLY_VERSION_TIMING_OFFSET = 24;
internal static RulesetStore RulesetStore; internal static RulesetStore RulesetStore;
private Beatmap beatmap; private Beatmap beatmap;
@ -50,8 +55,7 @@ namespace osu.Game.Beatmaps.Formats
RulesetStore = new AssemblyRulesetStore(); RulesetStore = new AssemblyRulesetStore();
} }
// BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) offset = FormatVersion < 5 ? EARLY_VERSION_TIMING_OFFSET : 0;
offset = FormatVersion < 5 ? 24 : 0;
} }
protected override Beatmap CreateTemplateObject() protected override Beatmap CreateTemplateObject()

View File

@ -23,6 +23,8 @@ namespace osu.Game.Scoring.Legacy
private IBeatmap currentBeatmap; private IBeatmap currentBeatmap;
private Ruleset currentRuleset; private Ruleset currentRuleset;
private float beatmapOffset;
public Score Parse(Stream stream) public Score Parse(Stream stream)
{ {
var score = new Score var score = new Score
@ -72,6 +74,9 @@ namespace osu.Game.Scoring.Legacy
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo; scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo;
// As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
/* score.HpGraphString = */ /* score.HpGraphString = */
sr.ReadString(); sr.ReadString();
@ -229,7 +234,7 @@ namespace osu.Game.Scoring.Legacy
private void readLegacyReplay(Replay replay, StreamReader reader) private void readLegacyReplay(Replay replay, StreamReader reader)
{ {
float lastTime = 0; float lastTime = beatmapOffset;
ReplayFrame currentFrame = null; ReplayFrame currentFrame = null;
string[] frames = reader.ReadToEnd().Split(','); string[] frames = reader.ReadToEnd().Split(',');

View File

@ -1,12 +1,15 @@
// 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.
#nullable enable
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO.Legacy; using osu.Game.IO.Legacy;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
@ -14,8 +17,6 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using SharpCompress.Compressors.LZMA; using SharpCompress.Compressors.LZMA;
#nullable enable
namespace osu.Game.Scoring.Legacy namespace osu.Game.Scoring.Legacy
{ {
public class LegacyScoreEncoder public class LegacyScoreEncoder
@ -111,6 +112,9 @@ namespace osu.Game.Scoring.Legacy
{ {
StringBuilder replayData = new StringBuilder(); StringBuilder replayData = new StringBuilder();
// As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
if (score.Replay != null) if (score.Replay != null)
{ {
int lastTime = 0; int lastTime = 0;
@ -120,7 +124,7 @@ namespace osu.Game.Scoring.Legacy
var legacyFrame = getLegacyFrame(f); var legacyFrame = getLegacyFrame(f);
// Rounding because stable could only parse integral values // Rounding because stable could only parse integral values
int time = (int)Math.Round(legacyFrame.Time); int time = (int)Math.Round(legacyFrame.Time + offset);
replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},"));
lastTime = time; lastTime = time;
} }