1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 20:13:21 +08:00
osu-lazer/osu.Game/Screens/Play/Player.cs

972 lines
36 KiB
C#
Raw Normal View History

2019-06-04 15:13:16 +08:00
// 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.
2018-04-13 17:19:50 +08:00
using System;
2019-04-09 12:33:16 +08:00
using System.Collections.Generic;
2020-03-24 13:13:46 +08:00
using System.IO;
2018-04-13 17:19:50 +08:00
using System.Linq;
using System.Threading.Tasks;
2020-12-17 15:17:13 +08:00
using JetBrains.Annotations;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
2018-10-02 11:02:47 +08:00
using osu.Framework.Input.Events;
2018-04-13 17:19:50 +08:00
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
2019-01-04 12:29:37 +08:00
using osu.Game.Graphics.Containers;
2020-03-24 13:13:46 +08:00
using osu.Game.IO.Archives;
2018-04-13 17:19:50 +08:00
using osu.Game.Online.API;
2018-06-06 14:10:09 +08:00
using osu.Game.Overlays;
2020-12-18 17:31:49 +08:00
using osu.Game.Replays;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
2020-12-18 17:31:49 +08:00
using osu.Game.Rulesets.Replays;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
2018-11-28 15:12:57 +08:00
using osu.Game.Scoring;
2020-03-24 13:13:46 +08:00
using osu.Game.Scoring.Legacy;
2018-04-13 17:19:50 +08:00
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
using osu.Game.Users;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Screens.Play
{
2019-11-01 14:32:06 +08:00
[Cached]
[Cached(typeof(ISamplePlaybackDisabler))]
public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler
2018-04-13 17:19:50 +08:00
{
/// <summary>
/// The delay upon completion of the beatmap before displaying the results screen.
/// </summary>
public const double RESULTS_DISPLAY_DELAY = 1000.0;
2019-06-25 15:55:49 +08:00
public override bool AllowBackButton => false; // handled by HoldForMenuButton
2018-04-13 17:19:50 +08:00
protected override UserActivity InitialActivity => new UserActivity.SoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
2018-04-13 17:19:50 +08:00
2019-01-23 19:52:00 +08:00
public override float BackgroundParallaxAmount => 0.1f;
2018-04-13 17:19:50 +08:00
public override bool HideOverlaysOnEnter => true;
2018-04-13 17:19:50 +08:00
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
2020-09-01 15:55:10 +08:00
// We are managing our own adjustments (see OnEntering/OnExiting).
public override bool AllowRateAdjustments => false;
2018-06-06 14:10:09 +08:00
private readonly IBindable<bool> gameActive = new Bindable<bool>(true);
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
2018-06-06 14:10:09 +08:00
/// <summary>
/// Whether gameplay should pause when the game window focus is lost.
/// </summary>
protected virtual bool PauseOnFocusLost => true;
2018-06-06 14:10:09 +08:00
2018-04-13 17:19:50 +08:00
public Action RestartRequested;
public bool HasFailed { get; private set; }
private Bindable<bool> mouseWheelDisabled;
2019-02-25 12:27:44 +08:00
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
2018-04-13 17:19:50 +08:00
2020-10-07 15:22:39 +08:00
protected readonly Bindable<bool> LocalUserPlaying = new Bindable<bool>();
2018-04-13 17:19:50 +08:00
public int RestartCount;
2018-11-29 13:56:29 +08:00
[Resolved]
private ScoreManager scoreManager { get; set; }
2018-04-13 17:19:50 +08:00
private RulesetInfo rulesetInfo;
2018-04-13 17:19:50 +08:00
private Ruleset ruleset;
2018-04-13 17:19:50 +08:00
2020-02-14 21:14:00 +08:00
[Resolved]
private IAPIProvider api { get; set; }
2018-04-13 17:19:50 +08:00
[Resolved]
private MusicController musicController { get; set; }
2021-01-19 16:11:40 +08:00
private Sample sampleRestart;
2018-04-13 17:19:50 +08:00
2019-12-12 14:14:59 +08:00
public BreakOverlay BreakOverlay;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Whether the gameplay is currently in a break.
/// </summary>
2020-10-11 20:46:55 +08:00
public readonly IBindable<bool> IsBreakTime = new BindableBool();
2020-10-10 21:07:17 +08:00
private BreakTracker breakTracker;
private SkipOverlay skipIntroOverlay;
private SkipOverlay skipOutroOverlay;
protected ScoreProcessor ScoreProcessor { get; private set; }
protected HealthProcessor HealthProcessor { get; private set; }
protected DrawableRuleset DrawableRuleset { get; private set; }
2018-04-13 17:19:50 +08:00
protected HUDOverlay HUDOverlay { get; private set; }
2018-04-13 17:19:50 +08:00
public bool LoadedBeatmapSuccessfully => DrawableRuleset?.Objects.Any() == true;
2018-04-13 17:19:50 +08:00
2019-03-17 23:46:15 +08:00
protected GameplayClockContainer GameplayClockContainer { get; private set; }
2018-04-13 17:19:50 +08:00
public DimmableStoryboard DimmableStoryboard { get; private set; }
2018-04-13 17:19:50 +08:00
2019-04-09 12:33:16 +08:00
[Cached]
2019-04-17 15:11:59 +08:00
[Cached(Type = typeof(IBindable<IReadOnlyList<Mod>>))]
2019-04-25 16:36:17 +08:00
protected new readonly Bindable<IReadOnlyList<Mod>> Mods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
2019-04-09 12:33:16 +08:00
2019-09-19 03:49:28 +08:00
/// <summary>
/// Whether failing should be allowed.
2019-09-19 13:31:11 +08:00
/// By default, this checks whether all selected mods allow failing.
2019-09-19 03:49:28 +08:00
/// </summary>
protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType<IApplicableFailOverride>().All(m => m.PerformFail());
2019-09-19 03:49:28 +08:00
public readonly PlayerConfiguration Configuration;
/// <summary>
/// Create a new player instance.
/// </summary>
protected Player(PlayerConfiguration configuration = null)
{
2020-12-23 17:07:38 +08:00
Configuration = configuration ?? new PlayerConfiguration();
}
2018-04-13 17:19:50 +08:00
private GameplayBeatmap gameplayBeatmap;
private ScreenSuspensionHandler screenSuspension;
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
2020-03-23 18:31:43 +08:00
protected override void LoadComplete()
{
base.LoadComplete();
// replays should never be recorded or played back when autoplay is enabled
if (!Mods.Value.Any(m => m is ModAutoplay))
PrepareReplay();
gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
2020-03-23 18:31:43 +08:00
}
2020-12-17 15:17:13 +08:00
[CanBeNull]
private Score recordingScore;
2020-03-23 18:31:43 +08:00
/// <summary>
/// Run any recording / playback setup for replays.
/// </summary>
protected virtual void PrepareReplay()
{
DrawableRuleset.SetRecordTarget(recordingScore = new Score());
ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(recordingScore.ScoreInfo);
2020-03-23 18:31:43 +08:00
}
[BackgroundDependencyLoader(true)]
2021-02-08 18:59:07 +08:00
private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
2018-04-13 17:19:50 +08:00
{
2019-04-17 15:11:59 +08:00
Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray();
2019-04-09 12:33:16 +08:00
if (Beatmap.Value is DummyWorkingBeatmap)
return;
IBeatmap playableBeatmap = loadPlayableBeatmap();
if (playableBeatmap == null)
2018-04-13 17:19:50 +08:00
return;
sampleRestart = audio.Samples.Get(@"Gameplay/restart");
2018-04-13 17:19:50 +08:00
mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
if (game != null)
gameActive.BindTo(game.IsActive);
2021-02-08 19:05:16 +08:00
2021-02-08 18:59:07 +08:00
if (game is OsuGame osuGame)
LocalUserPlaying.BindTo(osuGame.LocalUserPlaying);
DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value);
ScoreProcessor = ruleset.CreateScoreProcessor();
ScoreProcessor.ApplyBeatmap(playableBeatmap);
ScoreProcessor.Mods.BindTo(Mods);
2018-04-13 17:19:50 +08:00
dependencies.CacheAs(ScoreProcessor);
HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
HealthProcessor.ApplyBeatmap(playableBeatmap);
dependencies.CacheAs(HealthProcessor);
if (!ScoreProcessor.Mode.Disabled)
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
2018-04-13 17:19:50 +08:00
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
2018-04-13 17:19:50 +08:00
AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap));
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
dependencies.CacheAs(gameplayBeatmap);
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources.
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
// load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
GameplayClockContainer.Add(beatmapSkinProvider.WithChild(rulesetSkinProvider));
rulesetSkinProvider.AddRange(new[]
{
// underlay and gameplay should have access the to skinning sources.
createUnderlayComponents(),
createGameplayComponents(Beatmap.Value, playableBeatmap)
});
// also give the HUD a ruleset container to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
var hudRulesetContainer = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
GameplayClockContainer.Add(hudRulesetContainer.WithChild(createOverlayComponents(Beatmap.Value)));
2018-04-13 17:19:50 +08:00
if (!DrawableRuleset.AllowGameplayOverlays)
{
HUDOverlay.ShowHud.Value = false;
HUDOverlay.ShowHud.Disabled = true;
BreakOverlay.Hide();
}
2020-10-27 17:13:45 +08:00
DrawableRuleset.FrameStableClock.WaitingOnFrames.BindValueChanged(waiting =>
{
if (waiting.NewValue)
GameplayClockContainer.Stop();
else
GameplayClockContainer.Start();
});
2020-10-27 13:10:12 +08:00
DrawableRuleset.IsPaused.BindValueChanged(paused =>
{
updateGameplayState();
updateSampleDisabledState();
});
DrawableRuleset.FrameStableClock.IsCatchingUp.BindValueChanged(_ => updateSampleDisabledState());
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState());
2020-08-16 23:18:40 +08:00
// bind clock into components that require it
DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused);
2018-04-13 17:19:50 +08:00
DrawableRuleset.NewResult += r =>
{
HealthProcessor.ApplyResult(r);
ScoreProcessor.ApplyResult(r);
gameplayBeatmap.ApplyResult(r);
};
DrawableRuleset.RevertResult += r =>
{
HealthProcessor.RevertResult(r);
ScoreProcessor.RevertResult(r);
};
2019-12-11 16:25:06 +08:00
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
{
if (storyboardEnded.NewValue && completionProgressDelegate == null)
2021-04-19 13:58:19 +08:00
updateCompletionState();
};
// Bind the judgement processors to ourselves
2021-04-19 13:58:19 +08:00
ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState());
HealthProcessor.Failed += onFail;
2018-04-13 17:19:50 +08:00
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
mod.ApplyToScoreProcessor(ScoreProcessor);
foreach (var mod in Mods.Value.OfType<IApplicableToHealthProcessor>())
mod.ApplyToHealthProcessor(HealthProcessor);
2020-10-11 20:46:55 +08:00
IsBreakTime.BindTo(breakTracker.IsBreakTime);
IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
}
2018-04-13 17:19:50 +08:00
2021-04-14 16:47:11 +08:00
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
private Drawable createUnderlayComponents() =>
DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both };
2018-04-13 17:19:50 +08:00
private Drawable createGameplayComponents(WorkingBeatmap working, IBeatmap playableBeatmap) => new ScalingContainer(ScalingMode.Gameplay)
{
Children = new Drawable[]
{
DrawableRuleset.With(r =>
r.FrameStableComponents.Children = new Drawable[]
{
ScoreProcessor,
HealthProcessor,
new ComboEffects(ScoreProcessor),
breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
{
Breaks = working.Beatmap.Breaks
}
}),
}
};
2018-04-13 17:19:50 +08:00
private Drawable createOverlayComponents(WorkingBeatmap working)
{
var container = new Container
2018-04-13 17:19:50 +08:00
{
RelativeSizeAxes = Axes.Both,
Children = new[]
{
DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
2018-04-13 17:19:50 +08:00
{
Clock = DrawableRuleset.FrameStableClock,
ProcessCustomClock = false,
Breaks = working.Beatmap.Breaks
},
// display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
HUDOverlay = new HUDOverlay(DrawableRuleset, Mods.Value)
{
HoldToQuit =
{
Action = () => PerformExit(true),
IsPaused = { BindTarget = GameplayClockContainer.IsPaused }
},
KeyCounter =
{
AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded },
IsCounting = false
},
RequestSeek = time =>
{
GameplayClockContainer.Seek(time);
GameplayClockContainer.Start();
},
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime)
{
RequestSkip = performUserRequestedSkip
},
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
{
2021-04-19 13:58:19 +08:00
RequestSkip = () => updateCompletionState(true),
Alpha = 0
},
FailOverlay = new FailOverlay
2018-04-13 17:19:50 +08:00
{
OnRetry = Restart,
OnQuit = () => PerformExit(true),
},
PauseOverlay = new PauseOverlay
{
OnResume = Resume,
Retries = RestartCount,
OnRetry = Restart,
OnQuit = () => PerformExit(true),
},
new HotkeyExitOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
2018-04-13 17:19:50 +08:00
fadeOut(true);
PerformExit(false);
},
2018-04-13 17:19:50 +08:00
},
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
}
};
if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays)
{
skipIntroOverlay.Expire();
skipOutroOverlay.Expire();
}
2021-04-14 16:47:11 +08:00
if (GameplayClockContainer is MasterGameplayClockContainer master)
HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate;
if (Configuration.AllowRestart)
{
container.Add(new HotkeyRetryOverlay
2019-06-24 17:15:27 +08:00
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
fadeOut(true);
Restart();
2019-06-24 17:15:27 +08:00
},
});
}
return container;
}
2018-04-13 17:19:50 +08:00
private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime)
{
updateGameplayState();
updatePauseOnFocusLostState();
HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue;
}
private void updateGameplayState()
2020-08-04 03:25:45 +08:00
{
bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value;
OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered;
2020-10-07 15:22:39 +08:00
LocalUserPlaying.Value = inGameplay;
2020-08-04 03:25:45 +08:00
}
2020-10-27 13:10:12 +08:00
private void updateSampleDisabledState()
{
samplePlaybackDisabled.Value = DrawableRuleset.FrameStableClock.IsCatchingUp.Value || GameplayClockContainer.GameplayClock.IsPaused.Value;
}
private void updatePauseOnFocusLostState()
{
2021-02-22 21:59:35 +08:00
if (!PauseOnFocusLost || !pausingSupportedByCurrentState || breakTracker.IsBreakTime.Value)
return;
if (gameActive.Value == false)
{
bool paused = Pause();
// if the initial pause could not be satisfied, the pause cooldown may be active.
// reschedule the pause attempt until it can be achieved.
if (!paused)
2021-02-19 16:33:26 +08:00
Scheduler.AddOnce(updatePauseOnFocusLostState);
}
}
private IBeatmap loadPlayableBeatmap()
2018-04-13 17:19:50 +08:00
{
IBeatmap playable;
try
{
if (Beatmap.Value.Beatmap == null)
throw new InvalidOperationException("Beatmap was not loaded");
2018-04-13 17:19:50 +08:00
rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset;
ruleset = rulesetInfo.CreateInstance();
try
{
playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value);
}
catch (BeatmapInvalidForRulesetException)
{
// A playable beatmap may not be creatable with the user's preferred ruleset, so try using the beatmap's default ruleset
rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset;
ruleset = rulesetInfo.CreateInstance();
playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value);
2018-04-13 17:19:50 +08:00
}
if (playable.HitObjects.Count == 0)
{
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
return null;
}
}
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap successfully!");
//couldn't load, hard abort!
return null;
}
return playable;
2018-04-13 17:19:50 +08:00
}
2018-07-19 00:18:07 +08:00
/// <summary>
/// Exits the <see cref="Player"/>.
/// </summary>
2021-02-09 16:14:16 +08:00
/// <param name="showDialogFirst">
/// Whether the pause or fail dialog should be shown before performing an exit.
/// If true and a dialog is not yet displayed, the exit will be blocked the the relevant dialog will display instead.
/// </param>
2021-02-09 16:14:16 +08:00
protected void PerformExit(bool showDialogFirst)
{
2019-06-24 17:15:27 +08:00
// if a restart has been requested, cancel any pending completion (user has shown intent to restart).
2019-08-06 22:05:12 +08:00
completionProgressDelegate?.Cancel();
2018-04-13 17:19:50 +08:00
// there is a chance that the exit was performed after the transition to results has started.
// we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process).
if (!this.IsCurrentScreen())
{
ValidForResume = false;
this.MakeCurrent();
return;
}
2018-04-13 17:19:50 +08:00
bool pauseOrFailDialogVisible =
PauseOverlay.State.Value == Visibility.Visible || FailOverlay.State.Value == Visibility.Visible;
if (showDialogFirst && !pauseOrFailDialogVisible)
{
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
if (ValidForResume && HasFailed)
{
failAnimation.FinishTransforms(true);
return;
}
// there's a chance the pausing is not supported in the current state, at which point immediate exit should be preferred.
if (pausingSupportedByCurrentState)
{
// in the case a dialog needs to be shown, attempt to pause and show it.
// this may fail (see internal checks in Pause()) but the fail cases are temporary, so don't fall through to Exit().
Pause();
return;
}
// if the score is ready for display but results screen has not been pushed yet (e.g. storyboard is still playing beyond gameplay), then transition to results screen instead of exiting.
if (prepareScoreForDisplayTask != null && completionProgressDelegate == null)
{
2021-04-19 13:58:19 +08:00
updateCompletionState(true);
}
}
this.Exit();
2018-04-13 17:19:50 +08:00
}
private void performUserRequestedSkip()
{
// user requested skip
// disable sample playback to stop currently playing samples and perform skip
samplePlaybackDisabled.Value = true;
2021-04-14 16:47:11 +08:00
(GameplayClockContainer as MasterGameplayClockContainer)?.Skip();
// return samplePlaybackDisabled.Value to what is defined by the beatmap's current state
updateSampleDisabledState();
}
2019-11-01 14:51:45 +08:00
/// <summary>
/// Restart gameplay via a parent <see cref="PlayerLoader"/>.
/// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks>
/// </summary>
2018-04-13 17:19:50 +08:00
public void Restart()
{
if (!Configuration.AllowRestart)
return;
// at the point of restarting the track should either already be paused or the volume should be zero.
// stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader.
musicController.Stop();
2018-04-13 17:19:50 +08:00
sampleRestart?.Play();
RestartRequested?.Invoke();
2019-11-01 14:32:06 +08:00
2021-02-09 16:14:16 +08:00
PerformExit(false);
2018-04-13 17:19:50 +08:00
}
2019-08-06 22:05:12 +08:00
private ScheduledDelegate completionProgressDelegate;
private Task<ScoreInfo> prepareScoreForDisplayTask;
2018-04-13 17:19:50 +08:00
2021-05-04 15:43:51 +08:00
/// <summary>
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
/// </summary>
/// <param name="skipStoryboardOutro">If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it.</param>
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
2021-04-19 13:58:19 +08:00
private void updateCompletionState(bool skipStoryboardOutro = false)
2018-04-13 17:19:50 +08:00
{
// screen may be in the exiting transition phase.
if (!this.IsCurrentScreen())
return;
2021-04-19 13:58:19 +08:00
if (!ScoreProcessor.HasCompleted.Value)
{
completionProgressDelegate?.Cancel();
completionProgressDelegate = null;
ValidForResume = true;
skipOutroOverlay.Hide();
return;
}
if (completionProgressDelegate != null)
throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once");
2018-04-13 17:19:50 +08:00
// Only show the completion screen if the player hasn't failed
if (HealthProcessor.HasFailed)
2018-04-13 17:19:50 +08:00
return;
ValidForResume = false;
if (!Configuration.ShowResults) return;
2018-04-13 17:19:50 +08:00
prepareScoreForDisplayTask ??= Task.Run(async () =>
{
var score = CreateScore();
try
{
await PrepareScoreForResultsAsync(score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, "Score preparation failed!");
}
try
{
await ImportScore(score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, "Score import failed!");
}
return score.ScoreInfo;
});
if (skipStoryboardOutro)
{
scheduleCompletion();
return;
}
2021-05-04 15:35:50 +08:00
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
if (storyboardHasOutro)
{
skipOutroOverlay.Show();
return;
}
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
scheduleCompletion();
2018-04-13 17:19:50 +08:00
}
private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
{
if (!prepareScoreForDisplayTask.IsCompleted)
{
scheduleCompletion();
return;
}
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
this.Push(CreateResults(prepareScoreForDisplayTask.Result));
});
2019-03-18 10:48:11 +08:00
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
#region Fail Logic
protected FailOverlay FailOverlay { get; private set; }
2019-06-04 15:13:16 +08:00
private FailAnimation failAnimation;
2018-04-13 17:19:50 +08:00
private bool onFail()
{
if (!CheckModsAllowFailure())
2018-04-13 17:19:50 +08:00
return false;
HasFailed = true;
2019-03-18 10:48:11 +08:00
// There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer)
// could process an extra frame after the GameplayClock is stopped.
// In such cases we want the fail state to precede a user triggered pause.
if (PauseOverlay.State.Value == Visibility.Visible)
2019-03-18 10:48:11 +08:00
PauseOverlay.Hide();
failAnimation.Start();
if (Mods.Value.OfType<IApplicableFailOverride>().Any(m => m.RestartOnFail))
2018-10-14 23:18:52 +08:00
Restart();
2018-04-13 17:19:50 +08:00
return true;
}
2019-06-04 15:13:16 +08:00
// Called back when the transform finishes
private void onFailComplete()
{
GameplayClockContainer.Stop();
2019-03-18 10:48:11 +08:00
FailOverlay.Retries = RestartCount;
FailOverlay.Show();
2018-04-13 17:19:50 +08:00
}
2019-03-18 10:48:11 +08:00
#endregion
#region Pause Logic
public bool IsResuming { get; private set; }
/// <summary>
/// The amount of gameplay time after which a second pause is allowed.
/// </summary>
private const double pause_cooldown = 1000;
protected PauseOverlay PauseOverlay { get; private set; }
private double? lastPauseActionTime;
protected bool PauseCooldownActive =>
2021-02-19 14:34:39 +08:00
lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown;
/// <summary>
/// A set of conditionals which defines whether the current game state and configuration allows for
/// pausing to be attempted via <see cref="Pause"/>. If false, the game should generally exit if a user pause
/// is attempted.
/// </summary>
private bool pausingSupportedByCurrentState =>
2019-03-18 10:48:11 +08:00
// must pass basic screen conditions (beatmap loaded, instance allows pause)
LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume
2019-03-18 10:48:11 +08:00
// replays cannot be paused and exit immediately
&& !DrawableRuleset.HasReplayLoaded.Value
2019-03-18 10:48:11 +08:00
// cannot pause if we are already in a fail state
&& !HasFailed;
2019-03-18 10:48:11 +08:00
private bool canResume =>
// cannot resume from a non-paused state
GameplayClockContainer.IsPaused.Value
// cannot resume if we are already in a fail state
&& !HasFailed
// already resuming
&& !IsResuming;
public bool Pause()
2019-03-18 10:48:11 +08:00
{
if (!pausingSupportedByCurrentState) return false;
if (!IsResuming && PauseCooldownActive)
return false;
2019-03-18 10:48:11 +08:00
2019-10-26 03:57:49 +08:00
if (IsResuming)
{
DrawableRuleset.CancelResume();
2019-11-01 13:43:52 +08:00
IsResuming = false;
2019-10-26 03:57:49 +08:00
}
2019-03-18 10:48:11 +08:00
GameplayClockContainer.Stop();
PauseOverlay.Show();
lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime;
return true;
2019-03-18 10:48:11 +08:00
}
public void Resume()
{
if (!canResume) return;
IsResuming = true;
PauseOverlay.Hide();
// breaks and time-based conditions may allow instant resume.
if (breakTracker.IsBreakTime.Value)
completeResume();
else
DrawableRuleset.RequestResume(completeResume);
void completeResume()
{
GameplayClockContainer.Start();
IsResuming = false;
}
2019-03-18 10:48:11 +08:00
}
#endregion
#region Screen Logic
2019-01-23 19:52:00 +08:00
public override void OnEntering(IScreen last)
2018-04-13 17:19:50 +08:00
{
base.OnEntering(last);
if (!LoadedBeatmapSuccessfully)
2018-04-13 17:19:50 +08:00
return;
2019-01-23 19:52:00 +08:00
Alpha = 0;
this
2018-04-13 17:19:50 +08:00
.ScaleTo(0.7f)
.ScaleTo(1, 750, Easing.OutQuint)
.Delay(250)
.FadeIn(250);
ApplyToBackground(b =>
{
b.IgnoreUserSettings.Value = false;
b.BlurAmount.Value = 0;
// bind component bindables.
b.IsBreakTime.BindTo(breakTracker.IsBreakTime);
b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
});
2018-04-13 17:19:50 +08:00
HUDOverlay.IsBreakTime.BindTo(breakTracker.IsBreakTime);
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
2019-12-11 04:06:13 +08:00
DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
2018-04-13 17:19:50 +08:00
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
foreach (var mod in Mods.Value.OfType<IApplicableToPlayer>())
mod.ApplyToPlayer(this);
2019-06-29 09:23:59 +08:00
foreach (var mod in Mods.Value.OfType<IApplicableToHUD>())
mod.ApplyToHUD(HUDOverlay);
// Our mods are local copies of the global mods so they need to be re-applied to the track.
// This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack.
// Todo: In the future, player will receive in a track and will probably not have to worry about this...
musicController.ResetTrackAdjustments();
2020-09-01 15:55:10 +08:00
foreach (var mod in Mods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(musicController.CurrentTrack);
2020-09-04 03:56:47 +08:00
updateGameplayState();
GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint);
StartGameplay();
}
/// <summary>
/// Called to trigger the starting of the gameplay clock and underlying gameplay.
/// This will be called on entering the player screen once. A derived class may block the first call to this to delay the start of gameplay.
/// </summary>
protected virtual void StartGameplay()
{
if (GameplayClockContainer.GameplayClock.IsRunning)
throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running");
2021-04-19 18:53:55 +08:00
GameplayClockContainer.Reset();
2018-04-13 17:19:50 +08:00
}
2019-01-23 19:52:00 +08:00
public override void OnSuspending(IScreen next)
2018-04-13 17:19:50 +08:00
{
screenSuspension?.Expire();
2018-04-13 17:19:50 +08:00
fadeOut();
base.OnSuspending(next);
}
2019-01-23 19:52:00 +08:00
public override bool OnExiting(IScreen next)
2018-04-13 17:19:50 +08:00
{
screenSuspension?.Expire();
2019-08-06 22:05:12 +08:00
if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed)
{
2019-08-06 22:05:12 +08:00
// proceed to result screen if beatmap already finished playing
completionProgressDelegate.RunTask();
return true;
}
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
// as we are no longer the current screen, we cannot guarantee the track is still usable.
2021-04-14 16:47:11 +08:00
(GameplayClockContainer as MasterGameplayClockContainer)?.StopUsingBeatmapClock();
musicController.ResetTrackAdjustments();
2020-09-01 15:55:10 +08:00
fadeOut();
return base.OnExiting(next);
2018-04-13 17:19:50 +08:00
}
2020-12-18 16:47:33 +08:00
/// <summary>
/// Creates the player's <see cref="Score"/>.
/// </summary>
/// <returns>The <see cref="Score"/>.</returns>
protected virtual Score CreateScore()
2020-12-18 14:36:24 +08:00
{
var score = new Score
2020-12-18 14:36:24 +08:00
{
ScoreInfo = new ScoreInfo
{
Beatmap = Beatmap.Value.BeatmapInfo,
Ruleset = rulesetInfo,
Mods = Mods.Value.ToArray(),
}
2020-12-18 14:36:24 +08:00
};
if (DrawableRuleset.ReplayScore != null)
{
score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser();
score.Replay = DrawableRuleset.ReplayScore.Replay;
}
2020-12-18 14:36:24 +08:00
else
{
score.ScoreInfo.User = api.LocalUser.Value;
2020-12-18 17:31:49 +08:00
score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List<ReplayFrame>() };
}
2020-12-18 14:36:24 +08:00
ScoreProcessor.PopulateScore(score.ScoreInfo);
2020-12-18 14:36:24 +08:00
return score;
}
2020-12-18 16:47:33 +08:00
/// <summary>
/// Imports the player's <see cref="Score"/> to the local database.
2020-12-18 16:47:33 +08:00
/// </summary>
/// <param name="score">The <see cref="Score"/> to import.</param>
/// <returns>The imported score.</returns>
2020-12-19 12:58:56 +08:00
protected virtual Task ImportScore(Score score)
{
// Replays are already populated and present in the game's database, so should not be re-imported.
if (DrawableRuleset.ReplayScore != null)
2020-12-19 12:58:56 +08:00
return Task.CompletedTask;
LegacyByteArrayReader replayReader;
using (var stream = new MemoryStream())
{
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
2020-12-19 12:58:56 +08:00
return scoreManager.Import(score.ScoreInfo, replayReader);
}
/// <summary>
/// Prepare the <see cref="Score"/> for display at results.
/// </summary>
/// <param name="score">The <see cref="Score"/> to prepare.</param>
/// <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;
2020-12-18 16:47:33 +08:00
/// <summary>
/// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to be displayed in the results screen.</param>
/// <returns>The <see cref="ResultsScreen"/>.</returns>
2020-12-18 14:36:24 +08:00
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true);
private void fadeOut(bool instant = false)
2018-04-13 17:19:50 +08:00
{
float fadeOutDuration = instant ? 0 : 250;
2019-01-23 19:52:00 +08:00
this.FadeOut(fadeOutDuration);
2018-04-13 17:19:50 +08:00
ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
storyboardReplacesBackground.Value = false;
2018-04-13 17:19:50 +08:00
}
2019-03-18 10:48:11 +08:00
#endregion
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
2018-04-13 17:19:50 +08:00
}
}