1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 11:20:24 +08:00
Files
osu-lazer/osu.Game/Rulesets/UI/ReplayRecorder.cs
T
Bartłomiej Dach 9f91c2e25c Emit important replay frames on every judgement
- Closes https://github.com/ppy/osu/issues/4287
- Probably closes https://github.com/ppy/osu/issues/25405 (but not
  retroactively)

Up until now, whether or not a replay frame is emitted depended solely
on the user's input, i.e. mouse movement or key presses/releases. This,
intersected with the replay playback system which is given allowance to
perform interpolation between replay frames, leads to potential
situations wherein a replay can play inaccurately when a judgement takes
place without user input meaningfully changing. One such case is slider
ends with their 36ms of judgement leniency; see
https://github.com/ppy/osu/issues/25405#issuecomment-2879031106 for
details on that.

To that end, this commit aims to counteract that issue by *forcing* an
important replay frame to be emitted on every new judgement recorded
during gameplay. This will only benefit rulesets wherein judgements can
occur that are not inherently tied to user input changing, which are
going to be osu! as mentioned above, and maybe possibly catch. I don't
foresee this doing anything relevant for taiko or mania.
2025-05-15 11:43:09 +02:00

105 lines
2.9 KiB
C#

// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Replays;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Rulesets.UI
{
public abstract partial class ReplayRecorder<T> : ReplayRecorder, IKeyBindingHandler<T>
where T : struct
{
private readonly Score target;
private readonly List<T> pressedActions = new List<T>();
private InputManager inputManager;
/// <summary>
/// The frame rate to record replays at.
/// </summary>
public int RecordFrameRate { get; set; } = 60;
[Resolved]
private SpectatorClient spectatorClient { get; set; }
protected ReplayRecorder(Score target)
{
this.target = target;
RelativeSizeAxes = Axes.Both;
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
protected override void Update()
{
base.Update();
RecordFrame(false);
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
RecordFrame(false);
return base.OnMouseMove(e);
}
public bool OnPressed(KeyBindingPressEvent<T> e)
{
pressedActions.Add(e.Action);
RecordFrame(true);
return false;
}
public void OnReleased(KeyBindingReleaseEvent<T> e)
{
pressedActions.Remove(e.Action);
RecordFrame(true);
}
public override void RecordFrame(bool important)
{
var last = target.Replay.Frames.LastOrDefault();
if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate) * Clock.Rate)
return;
var position = ScreenSpaceToGamefield?.Invoke(inputManager.CurrentState.Mouse.Position) ?? inputManager.CurrentState.Mouse.Position;
var frame = HandleFrame(position, pressedActions, last);
if (frame != null)
{
target.Replay.Frames.Add(frame);
spectatorClient?.HandleFrame(frame);
}
}
protected abstract ReplayFrame HandleFrame(Vector2 mousePosition, List<T> actions, ReplayFrame previousFrame);
}
public abstract partial class ReplayRecorder : Component
{
public Func<Vector2, Vector2> ScreenSpaceToGamefield;
public abstract void RecordFrame(bool important);
}
}