1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 19:22:54 +08:00

Merge pull request #12952 from peppy/autoplay-pause-support

Externalise autoplay generation from `Player` to allow use of replay playback functions
This commit is contained in:
Dan Balasescu 2021-06-03 17:29:17 +09:00 committed by GitHub
commit d39eb7eac2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 203 additions and 127 deletions

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.EmptyFreeform.Objects;
using osu.Game.Rulesets.EmptyFreeform.Replays; using osu.Game.Rulesets.EmptyFreeform.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.EmptyFreeform.Mods namespace osu.Game.Rulesets.EmptyFreeform.Mods
{ {
public class EmptyFreeformModAutoplay : ModAutoplay<EmptyFreeformHitObject> public class EmptyFreeformModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -4,14 +4,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays; using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods namespace osu.Game.Rulesets.Pippidon.Mods
{ {
public class PippidonModAutoplay : ModAutoplay<PippidonHitObject> public class PippidonModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -3,7 +3,6 @@
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.EmptyScrolling.Objects;
using osu.Game.Rulesets.EmptyScrolling.Replays; using osu.Game.Rulesets.EmptyScrolling.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
@ -11,7 +10,7 @@ using System.Collections.Generic;
namespace osu.Game.Rulesets.EmptyScrolling.Mods namespace osu.Game.Rulesets.EmptyScrolling.Mods
{ {
public class EmptyScrollingModAutoplay : ModAutoplay<EmptyScrollingHitObject> public class EmptyScrollingModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -4,14 +4,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays; using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods namespace osu.Game.Rulesets.Pippidon.Mods
{ {
public class PippidonModAutoplay : ModAutoplay<PippidonHitObject> public class PippidonModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.Catch.Mods namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModAutoplay : ModAutoplay<CatchHitObject> public class CatchModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -4,7 +4,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -12,7 +11,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModAutoplay : ModAutoplay<ManiaHitObject> public class ManiaModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -6,14 +6,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAutoplay : ModAutoplay<OsuHitObject> public class OsuModAutoplay : ModAutoplay
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();

View File

@ -4,14 +4,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Rulesets.Taiko.Mods namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModAutoplay : ModAutoplay<TaikoHitObject> public class TaikoModAutoplay : ModAutoplay
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -18,12 +18,12 @@ namespace osu.Game.Tests.Visual.Gameplay
[Description("Player instantiated with an autoplay mod.")] [Description("Player instantiated with an autoplay mod.")]
public class TestSceneAutoplay : TestSceneAllRulesetPlayers public class TestSceneAutoplay : TestSceneAllRulesetPlayers
{ {
protected new TestPlayer Player => (TestPlayer)base.Player; protected new TestReplayPlayer Player => (TestReplayPlayer)base.Player;
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
SelectedMods.Value = new[] { ruleset.GetAutoplayMod() }; SelectedMods.Value = new[] { ruleset.GetAutoplayMod() };
return new TestPlayer(false); return new TestReplayPlayer(false);
} }
protected override void AddCheckSteps() protected override void AddCheckSteps()

View File

@ -12,6 +12,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -95,11 +96,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("set autoplay", () => Game.SelectedMods.Value = new[] { new OsuModAutoplay() }); AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } });
AddStep("press enter", () => InputManager.Key(Key.Enter)); AddStep("press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
AddStep("seek to end", () => player.ChildrenOfType<GameplayClockContainer>().First().Seek(beatmap().Track.Length)); AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning);
AddStep("seek to near end", () => player.ChildrenOfType<GameplayClockContainer>().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000));
AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded); AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded);
AddStep("attempt to retry", () => results.ChildrenOfType<HotkeyRetryOverlay>().First().Action()); AddStep("attempt to retry", () => results.ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player);

View File

@ -1,9 +1,11 @@
// 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.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
@ -52,15 +54,21 @@ namespace osu.Game.Rulesets.Edit
if (changeHandler != null) if (changeHandler != null)
{ {
// for now only regenerate replay on a finalised state change, not HitObjectUpdated. // for now only regenerate replay on a finalised state change, not HitObjectUpdated.
changeHandler.OnStateChange += updateReplay; changeHandler.OnStateChange += () => Scheduler.AddOnce(regenerateAutoplay);
} }
else else
{ {
beatmap.HitObjectUpdated += _ => updateReplay(); beatmap.HitObjectUpdated += _ => Scheduler.AddOnce(regenerateAutoplay);
} }
Scheduler.AddOnce(regenerateAutoplay);
} }
private void updateReplay() => Scheduler.AddOnce(drawableRuleset.RegenerateAutoplay); private void regenerateAutoplay()
{
var autoplayMod = drawableRuleset.Mods.OfType<ModAutoplay>().Single();
drawableRuleset.SetReplayScore(autoplayMod.CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
}
private void addHitObject(HitObject hitObject) private void addHitObject(HitObject hitObject)
{ {

View File

@ -0,0 +1,14 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
public interface ICreateReplay
{
public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods);
}
}

View File

@ -7,22 +7,11 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToDrawableRuleset<T> public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplay
where T : HitObject
{
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
{
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
}
}
public abstract class ModAutoplay : Mod, IApplicableFailOverride
{ {
public override string Name => "Autoplay"; public override string Name => "Autoplay";
public override string Acronym => "AT"; public override string Acronym => "AT";

View File

@ -182,18 +182,11 @@ namespace osu.Game.Rulesets.UI
.WithChild(ResumeOverlay))); .WithChild(ResumeOverlay)));
} }
RegenerateAutoplay(); applyRulesetMods(Mods, config);
loadObjects(cancellationToken ?? default); loadObjects(cancellationToken ?? default);
} }
public void RegenerateAutoplay()
{
// for now this is applying mods which aren't just autoplay.
// we'll need to reconsider this flow in the future.
applyRulesetMods(Mods, config);
}
/// <summary> /// <summary>
/// Creates and adds drawable representations of hit objects to the play field. /// Creates and adds drawable representations of hit objects to the play field.
/// </summary> /// </summary>

View File

@ -17,7 +17,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public class MultiSpectatorPlayer : SpectatorPlayer public class MultiSpectatorPlayer : SpectatorPlayer
{ {
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true); private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true);
private readonly Score score;
private readonly ISpectatorPlayerClock spectatorPlayerClock; private readonly ISpectatorPlayerClock spectatorPlayerClock;
/// <summary> /// <summary>
@ -28,7 +27,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock) public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock)
: base(score) : base(score)
{ {
this.score = score;
this.spectatorPlayerClock = spectatorPlayerClock; this.spectatorPlayerClock = spectatorPlayerClock;
} }
@ -43,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.UpdateAfterChildren(); base.UpdateAfterChildren();
// This is required because the frame stable clock is set to WaitingOnFrames = false for one frame. // This is required because the frame stable clock is set to WaitingOnFrames = false for one frame.
waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || score.Replay.Frames.Count == 0; waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
} }
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)

View File

@ -54,11 +54,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
} }
protected override Score CreateScore() protected override void PrepareScoreForResults()
{ {
var score = base.CreateScore(); base.PrepareScoreForResults();
score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
return score; Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -24,10 +23,8 @@ using osu.Game.IO.Archives;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -137,6 +134,8 @@ namespace osu.Game.Screens.Play
public readonly PlayerConfiguration Configuration; public readonly PlayerConfiguration Configuration;
protected Score Score { get; private set; }
/// <summary> /// <summary>
/// Create a new player instance. /// Create a new player instance.
/// </summary> /// </summary>
@ -145,7 +144,7 @@ namespace osu.Game.Screens.Play
Configuration = configuration ?? new PlayerConfiguration(); Configuration = configuration ?? new PlayerConfiguration();
} }
private GameplayBeatmap gameplayBeatmap; protected GameplayBeatmap GameplayBeatmap { get; private set; }
private ScreenSuspensionHandler screenSuspension; private ScreenSuspensionHandler screenSuspension;
@ -161,24 +160,32 @@ namespace osu.Game.Screens.Play
if (!LoadedBeatmapSuccessfully) if (!LoadedBeatmapSuccessfully)
return; return;
// replays should never be recorded or played back when autoplay is enabled Score = CreateScore();
if (!Mods.Value.Any(m => m is ModAutoplay))
PrepareReplay(); // ensure the score is in a consistent state with the current player.
Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo;
Score.ScoreInfo.Ruleset = rulesetInfo;
Score.ScoreInfo.Mods = Mods.Value.ToArray();
PrepareReplay();
ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo);
gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
} }
[CanBeNull]
private Score recordingScore;
/// <summary> /// <summary>
/// Run any recording / playback setup for replays. /// Run any recording / playback setup for replays.
/// </summary> /// </summary>
protected virtual void PrepareReplay() protected virtual void PrepareReplay()
{ {
DrawableRuleset.SetRecordTarget(recordingScore = new Score()); DrawableRuleset.SetRecordTarget(Score);
}
ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(recordingScore.ScoreInfo); protected virtual void PrepareScoreForResults()
{
// perform one final population to ensure everything is up-to-date.
ScoreProcessor.PopulateScore(Score.ScoreInfo);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
@ -223,10 +230,10 @@ namespace osu.Game.Screens.Play
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(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));
dependencies.CacheAs(gameplayBeatmap); dependencies.CacheAs(GameplayBeatmap);
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
@ -284,7 +291,7 @@ namespace osu.Game.Screens.Play
{ {
HealthProcessor.ApplyResult(r); HealthProcessor.ApplyResult(r);
ScoreProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r);
gameplayBeatmap.ApplyResult(r); GameplayBeatmap.ApplyResult(r);
}; };
DrawableRuleset.RevertResult += r => DrawableRuleset.RevertResult += r =>
@ -633,11 +640,11 @@ namespace osu.Game.Screens.Play
prepareScoreForDisplayTask ??= Task.Run(async () => prepareScoreForDisplayTask ??= Task.Run(async () =>
{ {
var score = CreateScore(); PrepareScoreForResults();
try try
{ {
await PrepareScoreForResultsAsync(score).ConfigureAwait(false); await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -646,14 +653,14 @@ namespace osu.Game.Screens.Play
try try
{ {
await ImportScore(score).ConfigureAwait(false); await ImportScore(Score).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error(ex, "Score import failed!"); Logger.Error(ex, "Score import failed!");
} }
return score.ScoreInfo; return Score.ScoreInfo;
}); });
if (skipStoryboardOutro) if (skipStoryboardOutro)
@ -905,41 +912,19 @@ namespace osu.Game.Screens.Play
} }
/// <summary> /// <summary>
/// Creates the player's <see cref="Score"/>. /// Creates the player's <see cref="Scoring.Score"/>.
/// </summary> /// </summary>
/// <returns>The <see cref="Score"/>.</returns> /// <returns>The <see cref="Scoring.Score"/>.</returns>
protected virtual Score CreateScore() protected virtual Score CreateScore() =>
{ new Score
var score = new Score
{ {
ScoreInfo = new ScoreInfo ScoreInfo = new ScoreInfo { User = api.LocalUser.Value },
{
Beatmap = Beatmap.Value.BeatmapInfo,
Ruleset = rulesetInfo,
Mods = Mods.Value.ToArray(),
}
}; };
if (DrawableRuleset.ReplayScore != null)
{
score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser();
score.Replay = DrawableRuleset.ReplayScore.Replay;
}
else
{
score.ScoreInfo.User = api.LocalUser.Value;
score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List<ReplayFrame>() };
}
ScoreProcessor.PopulateScore(score.ScoreInfo);
return score;
}
/// <summary> /// <summary>
/// Imports the player's <see cref="Score"/> to the local database. /// Imports the player's <see cref="Scoring.Score"/> to the local database.
/// </summary> /// </summary>
/// <param name="score">The <see cref="Score"/> to import.</param> /// <param name="score">The <see cref="Scoring.Score"/> to import.</param>
/// <returns>The imported score.</returns> /// <returns>The imported score.</returns>
protected virtual async Task ImportScore(Score score) protected virtual async Task ImportScore(Score score)
{ {
@ -951,7 +936,7 @@ namespace osu.Game.Screens.Play
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); new LegacyScoreEncoder(score, GameplayBeatmap.PlayableBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
} }
@ -970,9 +955,9 @@ namespace osu.Game.Screens.Play
} }
/// <summary> /// <summary>
/// Prepare the <see cref="Score"/> for display at results. /// Prepare the <see cref="Scoring.Score"/> for display at results.
/// </summary> /// </summary>
/// <param name="score">The <see cref="Score"/> to prepare.</param> /// <param name="score">The <see cref="Scoring.Score"/> to prepare.</param>
/// <returns>A task that prepares the provided score. On completion, the score is assumed to be ready for display.</returns> /// <returns>A task that prepares the provided score. On completion, the score is assumed to be ready for display.</returns>
protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask; protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask;

View File

@ -1,9 +1,13 @@
// 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.
using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -11,15 +15,20 @@ namespace osu.Game.Screens.Play
{ {
public class ReplayPlayer : Player, IKeyBindingHandler<GlobalAction> public class ReplayPlayer : Player, IKeyBindingHandler<GlobalAction>
{ {
protected readonly Score Score; private readonly Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore;
// 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;
public ReplayPlayer(Score score, PlayerConfiguration configuration = null) public ReplayPlayer(Score score, PlayerConfiguration configuration = null)
: this((_, __) => score, configuration)
{
}
public ReplayPlayer(Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore, PlayerConfiguration configuration = null)
: base(configuration) : base(configuration)
{ {
Score = score; this.createScore = createScore;
} }
protected override void PrepareReplay() protected override void PrepareReplay()
@ -27,15 +36,7 @@ namespace osu.Game.Screens.Play
DrawableRuleset?.SetReplayScore(Score); DrawableRuleset?.SetReplayScore(Score);
} }
protected override Score CreateScore() protected override Score CreateScore() => createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value);
{
var baseScore = base.CreateScore();
// Since the replay score doesn't contain statistics, we'll pass them through here.
Score.ScoreInfo.HitEvents = baseScore.ScoreInfo.HitEvents;
return Score;
}
// Don't re-import replay scores as they're already present in the database. // Don't re-import replay scores as they're already present in the database.
protected override Task ImportScore(Score score) => Task.CompletedTask; protected override Task ImportScore(Score score) => Task.CompletedTask;

View File

@ -18,6 +18,9 @@ namespace osu.Game.Screens.Play
{ {
private readonly Score score; private readonly Score score;
[Resolved]
private SpectatorClient spectatorClient { get; set; }
protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap
public SpectatorPlayer(Score score) public SpectatorPlayer(Score score)
@ -25,13 +28,10 @@ namespace osu.Game.Screens.Play
this.score = score; this.score = score;
} }
protected override ResultsScreen CreateResults(ScoreInfo score) protected override Score CreateScore() => score;
{
return new SpectatorResultsScreen(score);
}
[Resolved] protected override ResultsScreen CreateResults(ScoreInfo score)
private SpectatorClient spectatorClient { get; set; } => new SpectatorResultsScreen(score);
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

View File

@ -21,7 +21,7 @@ namespace osu.Game.Screens.Select
public class PlaySongSelect : SongSelect public class PlaySongSelect : SongSelect
{ {
private bool removeAutoModOnResume; private bool removeAutoModOnResume;
private OsuScreen player; private OsuScreen playerLoader;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private NotificationOverlay notifications { get; set; } private NotificationOverlay notifications { get; set; }
@ -49,7 +49,7 @@ namespace osu.Game.Screens.Select
{ {
base.OnResuming(last); base.OnResuming(last);
player = null; playerLoader = null;
if (removeAutoModOnResume) if (removeAutoModOnResume)
{ {
@ -79,14 +79,14 @@ namespace osu.Game.Screens.Select
protected override bool OnStart() protected override bool OnStart()
{ {
if (player != null) return false; if (playerLoader != null) return false;
// Ctrl+Enter should start map with autoplay enabled. // Ctrl+Enter should start map with autoplay enabled.
if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true) if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true)
{ {
var autoplayMod = getAutoplayMod(); var autoInstance = getAutoplayMod();
if (autoplayMod == null) if (autoInstance == null)
{ {
notifications?.Post(new SimpleNotification notifications?.Post(new SimpleNotification
{ {
@ -97,18 +97,26 @@ namespace osu.Game.Screens.Select
var mods = Mods.Value; var mods = Mods.Value;
if (mods.All(m => m.GetType() != autoplayMod.GetType())) if (mods.All(m => m.GetType() != autoInstance.GetType()))
{ {
Mods.Value = mods.Append(autoplayMod).ToArray(); Mods.Value = mods.Append(autoInstance).ToArray();
removeAutoModOnResume = true; removeAutoModOnResume = true;
} }
} }
SampleConfirm?.Play(); SampleConfirm?.Play();
this.Push(player = new PlayerLoader(() => new SoloPlayer())); this.Push(playerLoader = new PlayerLoader(createPlayer));
return true; return true;
Player createPlayer()
{
var replayGeneratingMod = Mods.Value.OfType<ICreateReplay>().FirstOrDefault();
if (replayGeneratingMod != null)
return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods));
return new SoloPlayer();
}
} }
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -48,6 +49,26 @@ namespace osu.Game.Tests.Visual
PauseOnFocusLost = pauseOnFocusLost; PauseOnFocusLost = pauseOnFocusLost;
} }
protected override void PrepareReplay()
{
// Generally, replay generation is handled by whatever is constructing the player.
// This is implemented locally here to ease migration of test scenes that have some executions
// running with autoplay and some not, but are not written in a way that lends to instantiating
// different `Player` types.
//
// Eventually we will want to remove this and update all test usages which rely on autoplay to use
// a `TestReplayPlayer`.
var autoplayMod = Mods.Value.OfType<ModAutoplay>().FirstOrDefault();
if (autoplayMod != null)
{
DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value));
return;
}
base.PrepareReplay();
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {

View File

@ -0,0 +1,65 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual
{
/// <summary>
/// A player that exposes many components that would otherwise not be available, for testing purposes.
/// </summary>
public class TestReplayPlayer : ReplayPlayer
{
protected override bool PauseOnFocusLost { get; }
public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
/// <summary>
/// Mods from *player* (not OsuScreen).
/// </summary>
public new Bindable<IReadOnlyList<Mod>> Mods => base.Mods;
public new HUDOverlay HUDOverlay => base.HUDOverlay;
public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public new HealthProcessor HealthProcessor => base.HealthProcessor;
public new bool PauseCooldownActive => base.PauseCooldownActive;
/// <summary>
/// Instantiate a replay player that renders an autoplay mod.
/// </summary>
public TestReplayPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
: base((beatmap, mods) => mods.OfType<ModAutoplay>().First().CreateReplayScore(beatmap, mods), new PlayerConfiguration
{
AllowPause = allowPause,
ShowResults = showResults
})
{
PauseOnFocusLost = pauseOnFocusLost;
}
/// <summary>
/// Instantiate a replay player that renders the provided replay.
/// </summary>
public TestReplayPlayer(Score score, bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
: base(score, new PlayerConfiguration
{
AllowPause = allowPause,
ShowResults = showResults
})
{
PauseOnFocusLost = pauseOnFocusLost;
}
}
}