1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-24 05:29:57 +08:00

Ensure partial failed replays are played to their end

Closes https://github.com/ppy/osu/issues/24285.

This is not a perfect solution, as it is still possible for a replay to
play *beyond* its end if the HP system doesn't fail it after it runs out
of frames, but it's probably the best that can be done at this time.

Notably this removes existing F rank checks because they were really not
reliable.

- Scores coming from stable will never present F rank, because rank is
  not stored to the replay, and the lowest rank that can be produced by
  `StandardisedScoreMigrationTools` is D.

- lazer scores set prior to https://github.com/ppy/osu/pull/28058 will
  present F rank as long as the user has kept them in their local
  database and never exported and reimported them, for the same reason
  as above (rank not stored to replay). Also there have been many
  mechanics changes since, so it's not impossible for the replay to fail
  *before* the user actually did even in this case.

- lazer scores set after https://github.com/ppy/osu/pull/28058 could
  technically rely on F rank but making them rely on it is annoying for
  several reasons:

  - The PR in question didn't bump `LegacyScoreEncoder.LATEST_VERSION`,
    so any checks based on the replay version field would be
    half-reliable anyway.

  - *Even after* the above, the replay version is only stored to realm
    as `TotalScoreVersion`, which *then gets bumped* on score version
    upgrades. So it can't even be used for any checks from that angle,
    you'd have to decode it from the score.

  - You *could* use `ClientVersion` because that's somewhat reliable,
    but that's stored *as string*, so you'd have to do some snipping to
    split off the `-lazer` suffix, then parse the version, then compare
    it. I started going through the motions of that before deciding that
    this is an edge case of an edge case and probably not worth spending
    time over the simple and obvious solution of just doing away with
    the rank check. Until I'm proven wrong, I guess.
This commit is contained in:
Bartłomiej Dach
2025-06-12 14:03:55 +02:00
Unverified
parent e59f9b1aa7
commit 73a1f10daf
+15 -7
View File
@@ -29,8 +29,6 @@ namespace osu.Game.Screens.Play
private readonly Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore;
private readonly bool replayIsFailedScore;
private PlaybackSettings playbackSettings;
[Cached(typeof(IGameplayLeaderboardProvider))]
@@ -40,19 +38,28 @@ namespace osu.Game.Screens.Play
private bool isAutoplayPlayback => GameplayState.Mods.OfType<ModAutoplay>().Any();
// Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108)
private double? lastFrameTime;
protected override bool CheckModsAllowFailure()
{
if (!replayIsFailedScore && !isAutoplayPlayback)
return false;
// autoplay should be able to fail if the beatmap is not humanly beatable
if (isAutoplayPlayback)
return base.CheckModsAllowFailure();
return base.CheckModsAllowFailure();
// non-autoplay replays should be able to fail, but only after they've exhausted their frames.
// note that the rank isn't checked here - that's because it is generally unreliable.
// stable replays, as well as lazer replays recorded prior to https://github.com/ppy/osu/pull/28058,
// do not even *contain* the user's rank.
// not to mention possible gameplay mechanics changes that could make a replay fail sooner than it really should.
if (GameplayClockContainer.CurrentTime >= lastFrameTime)
return base.CheckModsAllowFailure();
return false;
}
public ReplayPlayer(Score score, PlayerConfiguration configuration = null)
: this((_, _) => score, configuration)
{
replayIsFailedScore = score.ScoreInfo.Rank == ScoreRank.F;
}
public ReplayPlayer(Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore, PlayerConfiguration configuration = null)
@@ -95,6 +102,7 @@ namespace osu.Game.Screens.Play
protected override void PrepareReplay()
{
DrawableRuleset?.SetReplayScore(Score);
lastFrameTime = Score.Replay.Frames.LastOrDefault()?.Time;
}
protected override Score CreateScore(IBeatmap beatmap) => createScore(beatmap, Mods.Value);