1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-17 07:22:38 +08:00

Merge pull request #33491 from bdach/fix-replays

Fix replays being misrecorded if an action is pressed and released in one update frame
This commit is contained in:
Dean Herbert
2025-06-06 16:35:55 +09:00
committed by GitHub
Unverified
16 changed files with 69 additions and 6 deletions
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
using osuTK;
@@ -17,5 +18,8 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
if (button.HasValue)
Actions.Add(button.Value);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is EmptyFreeformReplayFrame freeformFrame && Time == freeformFrame.Time && Position == freeformFrame.Position && Actions.SequenceEqual(freeformFrame.Actions);
}
}
@@ -9,5 +9,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
public class PippidonReplayFrame : ReplayFrame
{
public Vector2 Position;
public override bool IsEquivalentTo(ReplayFrame other)
=> other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Position == pippidonFrame.Position;
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.EmptyScrolling.Replays
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays
if (button.HasValue)
Actions.Add(button.Value);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is EmptyScrollingReplayFrame scrollingFrame && Time == scrollingFrame.Time && Actions.SequenceEqual(scrollingFrame.Actions);
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Pippidon.Replays
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
if (button.HasValue)
Actions.Add(button.Value);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Actions.SequenceEqual(pippidonFrame.Actions);
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -64,5 +65,12 @@ namespace osu.Game.Rulesets.Catch.Replays
return new LegacyReplayFrame(Time, Position, null, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is CatchReplayFrame catchFrame
&& Time == catchFrame.Time
&& Position == catchFrame.Position
&& Dashing == catchFrame.Dashing
&& Actions.SequenceEqual(catchFrame.Actions);
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -47,5 +48,8 @@ namespace osu.Game.Rulesets.Mania.Replays
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is ManiaReplayFrame maniaFrame && Time == maniaFrame.Time && Actions.SequenceEqual(maniaFrame.Actions);
}
}
@@ -78,6 +78,16 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("smoke button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.Smoke])));
}
[Test]
public void TestPressAndReleaseOnSameFrame()
{
seekTo(0);
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
AddStep("press X", () => InputManager.PressKey(Key.X));
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton])));
}
private void seekTo(double time)
{
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -47,5 +48,8 @@ namespace osu.Game.Rulesets.Osu.Replays
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is OsuReplayFrame osuFrame && Time == osuFrame.Time && Position == osuFrame.Position && Actions.SequenceEqual(osuFrame.Actions);
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
@@ -42,5 +43,8 @@ namespace osu.Game.Rulesets.Taiko.Replays
return new LegacyReplayFrame(Time, null, null, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is TaikoReplayFrame taikoFrame && Time == taikoFrame.Time && Actions.SequenceEqual(taikoFrame.Actions);
}
}
@@ -383,6 +383,9 @@ namespace osu.Game.Tests.NonVisual
IsImportant = isImportant;
FrameIndex = frameIndex;
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is TestReplayFrame testFrame && Time == testFrame.Time && IsImportant == testFrame.IsImportant && FrameIndex == testFrame.FrameIndex;
}
private class TestInputHandler : FramedReplayInputHandler<TestReplayFrame>
@@ -317,6 +317,9 @@ namespace osu.Game.Tests.Visual.Gameplay
Position = position;
Actions.AddRange(actions);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is TestReplayFrame testFrame && Time == testFrame.Time && Position == testFrame.Position && Actions.SequenceEqual(testFrame.Actions);
}
public enum TestAction
@@ -353,6 +353,9 @@ namespace osu.Game.Tests.Visual.Gameplay
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is TestReplayFrame testFrame && Time == testFrame.Time && Position == testFrame.Position && Actions.SequenceEqual(testFrame.Actions);
}
public enum TestAction
+2 -4
View File
@@ -247,12 +247,10 @@ namespace osu.Game.Online.Spectator
var convertedFrame = convertible.ToLegacy(currentBeatmap);
// only keep the last recorded frame for a given timestamp.
// this reduces redundancy of frames in the resulting replay.
//
// this is also done at `ReplayRecorded`, but needs to be done here as well
// it is also done at `ReplayRecorder`, but needs to be done here as well
// due to the flow being handled differently.
if (pendingFrames.LastOrDefault()?.Time == convertedFrame.Time)
if (pendingFrames.LastOrDefault()?.IsEquivalentTo(convertedFrame) == true)
pendingFrames[^1] = convertedFrame;
else
pendingFrames.Add(convertedFrame);
@@ -64,5 +64,12 @@ namespace osu.Game.Replays.Legacy
{
return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}";
}
public override bool IsEquivalentTo(ReplayFrame other)
=> other is LegacyReplayFrame legacyFrame
&& Time == legacyFrame.Time
&& MouseX == legacyFrame.MouseX
&& MouseY == legacyFrame.MouseY
&& ButtonState == legacyFrame.ButtonState;
}
}
+5
View File
@@ -30,5 +30,10 @@ namespace osu.Game.Rulesets.Replays
{
Time = time;
}
/// <summary>
/// Whether this frame is equivalent to <paramref name="other"/> with respect to replay recording.
/// </summary>
public virtual bool IsEquivalentTo(ReplayFrame other) => Time == other.Time;
}
}
+1 -2
View File
@@ -86,9 +86,8 @@ namespace osu.Game.Rulesets.UI
if (frame != null)
{
// only keep the last recorded frame for a given timestamp.
// this reduces redundancy of frames in the resulting replay.
if (last?.Time == frame.Time)
if (last?.IsEquivalentTo(frame) == true)
target.Replay.Frames[^1] = frame;
else
target.Replay.Frames.Add(frame);