Fix spectating when starting from a point that isn't at the beginning of the beatmap

This commit is contained in:
Dean Herbert
2020-10-27 18:56:28 +09:00
parent a289b7034f
commit 42b3aa3359
7 changed files with 93 additions and 18 deletions

View File

@ -93,9 +93,15 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestSpectatingDuringGameplay() public void TestSpectatingDuringGameplay()
{ {
start(); start();
sendFrames();
// should seek immediately to available frames
loadSpectatingScreen(); loadSpectatingScreen();
AddStep("advance frame count", () => nextFrame = 300);
sendFrames();
waitForPlayer();
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime > 30000);
} }
[Test] [Test]

View File

@ -37,6 +37,7 @@ namespace osu.Game.Screens.Play
private readonly DecoupleableInterpolatingFramedClock adjustableClock; private readonly DecoupleableInterpolatingFramedClock adjustableClock;
private readonly double gameplayStartTime; private readonly double gameplayStartTime;
private readonly bool startAtGameplayStart;
private readonly double firstHitObjectTime; private readonly double firstHitObjectTime;
@ -62,10 +63,11 @@ namespace osu.Game.Screens.Play
private readonly FramedOffsetClock platformOffsetClock; private readonly FramedOffsetClock platformOffsetClock;
public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime) public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
this.gameplayStartTime = gameplayStartTime; this.gameplayStartTime = gameplayStartTime;
this.startAtGameplayStart = startAtGameplayStart;
track = beatmap.Track; track = beatmap.Track;
firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
@ -103,16 +105,21 @@ namespace osu.Game.Screens.Play
userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true);
// sane default provided by ruleset. // sane default provided by ruleset.
double startTime = Math.Min(0, gameplayStartTime); double startTime = gameplayStartTime;
// if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. if (!startAtGameplayStart)
// this is commonly used to display an intro before the audio track start. {
startTime = Math.Min(startTime, beatmap.Storyboard.FirstEventTime); startTime = Math.Min(0, startTime);
// some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space.
// this is not available as an option in the live editor but can still be applied via .osu editing. // this is commonly used to display an intro before the audio track start.
if (beatmap.BeatmapInfo.AudioLeadIn > 0) startTime = Math.Min(startTime, beatmap.Storyboard.FirstEventTime);
startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn);
// some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available.
// this is not available as an option in the live editor but can still be applied via .osu editing.
if (beatmap.BeatmapInfo.AudioLeadIn > 0)
startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn);
}
Seek(startTime); Seek(startTime);

View File

@ -199,7 +199,7 @@ namespace osu.Game.Screens.Play
if (!ScoreProcessor.Mode.Disabled) if (!ScoreProcessor.Mode.Disabled)
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap));
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
@ -288,6 +288,8 @@ namespace osu.Game.Screens.Play
IsBreakTime.BindValueChanged(onBreakTimeChanged, true); IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
} }
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new GameplayClockContainer(beatmap, gameplayStart);
private Drawable createUnderlayComponents() => private Drawable createUnderlayComponents() =>
DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both };

View File

@ -8,7 +8,7 @@ namespace osu.Game.Screens.Play
{ {
public class ReplayPlayer : Player public class ReplayPlayer : Player
{ {
private readonly Score score; protected readonly Score Score;
// Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108)
protected override bool CheckModsAllowFailure() => false; protected override bool CheckModsAllowFailure() => false;
@ -16,12 +16,12 @@ namespace osu.Game.Screens.Play
public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true) public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true)
: base(allowPause, showResults) : base(allowPause, showResults)
{ {
this.score = score; this.Score = score;
} }
protected override void PrepareReplay() protected override void PrepareReplay()
{ {
DrawableRuleset?.SetReplayScore(score); DrawableRuleset?.SetReplayScore(Score);
} }
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
@ -31,9 +31,9 @@ namespace osu.Game.Screens.Play
var baseScore = base.CreateScore(); var baseScore = base.CreateScore();
// Since the replay score doesn't contain statistics, we'll pass them through here. // Since the replay score doesn't contain statistics, we'll pass them through here.
score.ScoreInfo.HitEvents = baseScore.HitEvents; Score.ScoreInfo.HitEvents = baseScore.HitEvents;
return score.ScoreInfo; return Score.ScoreInfo;
} }
} }
} }

View File

@ -130,7 +130,7 @@ namespace osu.Game.Screens.Play
ruleset.Value = resolvedRuleset.RulesetInfo; ruleset.Value = resolvedRuleset.RulesetInfo;
beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap);
this.Push(new ReplayPlayerLoader(new Score this.Push(new SpectatorPlayerLoader(new Score
{ {
ScoreInfo = scoreInfo, ScoreInfo = scoreInfo,
Replay = replay, Replay = replay,

View File

@ -0,0 +1,28 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Scoring;
namespace osu.Game.Screens.Play
{
public class SpectatorPlayer : ReplayPlayer
{
public SpectatorPlayer(Score score)
: base(score)
{
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
{
// if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap.
double? firstFrameTime = Score.Replay.Frames.FirstOrDefault()?.Time;
if (firstFrameTime == null || firstFrameTime <= gameplayStart + 5000)
return base.CreateGameplayClockContainer(beatmap, gameplayStart);
return new GameplayClockContainer(beatmap, firstFrameTime.Value, true);
}
}
}

View File

@ -0,0 +1,32 @@
// 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;
using osu.Framework.Screens;
using osu.Game.Scoring;
namespace osu.Game.Screens.Play
{
public class SpectatorPlayerLoader : PlayerLoader
{
public readonly ScoreInfo Score;
public SpectatorPlayerLoader(Score score)
: base(() => new SpectatorPlayer(score))
{
if (score.Replay == null)
throw new ArgumentException($"{nameof(score)} must have a non-null {nameof(score.Replay)}.", nameof(score));
Score = score.ScoreInfo;
}
public override void OnEntering(IScreen last)
{
// these will be reverted thanks to PlayerLoader's lease.
Mods.Value = Score.Mods;
Ruleset.Value = Score.Ruleset;
base.OnEntering(last);
}
}
}