mirror of
https://github.com/osukey/osukey.git
synced 2025-08-03 22:56:36 +09:00
Merge branch 'master' into transformers-per-skin
This commit is contained in:
@ -181,12 +181,6 @@ namespace osu.Game.Screens.Play
|
||||
DrawableRuleset.SetRecordTarget(Score);
|
||||
}
|
||||
|
||||
protected virtual void PrepareScoreForResults()
|
||||
{
|
||||
// perform one final population to ensure everything is up-to-date.
|
||||
ScoreProcessor.PopulateScore(Score.ScoreInfo);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
|
||||
{
|
||||
@ -295,12 +289,12 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
|
||||
{
|
||||
if (storyboardEnded.NewValue && completionProgressDelegate == null)
|
||||
updateCompletionState();
|
||||
if (storyboardEnded.NewValue)
|
||||
progressToResults(true);
|
||||
};
|
||||
|
||||
// Bind the judgement processors to ourselves
|
||||
ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState());
|
||||
ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged);
|
||||
HealthProcessor.Failed += onFail;
|
||||
|
||||
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
|
||||
@ -376,7 +370,7 @@ namespace osu.Game.Screens.Play
|
||||
},
|
||||
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
|
||||
{
|
||||
RequestSkip = () => updateCompletionState(true),
|
||||
RequestSkip = () => progressToResults(false),
|
||||
Alpha = 0
|
||||
},
|
||||
FailOverlay = new FailOverlay
|
||||
@ -508,19 +502,25 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exits the <see cref="Player"/>.
|
||||
/// Attempts to complete a user request to exit gameplay.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item>This should only be called in response to a user interaction. Exiting is not guaranteed.</item>
|
||||
/// <item>This will interrupt any pending progression to the results screen, even if the transition has begun.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <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.
|
||||
/// If <see langword="true"/> and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead.
|
||||
/// </param>
|
||||
protected void PerformExit(bool showDialogFirst)
|
||||
{
|
||||
// if a restart has been requested, cancel any pending completion (user has shown intent to restart).
|
||||
completionProgressDelegate?.Cancel();
|
||||
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
// 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).
|
||||
// there is a chance that an exit request occurs after the transition to results has already started.
|
||||
// even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process).
|
||||
if (!this.IsCurrentScreen())
|
||||
{
|
||||
ValidForResume = false;
|
||||
@ -543,7 +543,7 @@ namespace osu.Game.Screens.Play
|
||||
return;
|
||||
}
|
||||
|
||||
// there's a chance the pausing is not supported in the current state, at which point immediate exit should be preferred.
|
||||
// even if this call has requested a dialog, there is a chance the current player mode doesn't support pausing.
|
||||
if (pausingSupportedByCurrentState)
|
||||
{
|
||||
// in the case a dialog needs to be shown, attempt to pause and show it.
|
||||
@ -551,14 +551,12 @@ namespace osu.Game.Screens.Play
|
||||
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)
|
||||
{
|
||||
updateCompletionState(true);
|
||||
}
|
||||
}
|
||||
|
||||
// The actual exit is performed if
|
||||
// - the pause / fail dialog was not requested
|
||||
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
|
||||
// - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance.
|
||||
this.Exit();
|
||||
}
|
||||
|
||||
@ -622,98 +620,141 @@ namespace osu.Game.Screens.Play
|
||||
PerformExit(false);
|
||||
}
|
||||
|
||||
private ScheduledDelegate completionProgressDelegate;
|
||||
/// <summary>
|
||||
/// This delegate, when set, means the results screen has been queued to appear.
|
||||
/// The display of the results screen may be delayed by any work being done in <see cref="PrepareScoreForResultsAsync"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Once set, this can *only* be cancelled by rewinding, ie. if <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="false"/>.
|
||||
/// Even if the user requests an exit, it will forcefully proceed to the results screen (see special case in <see cref="OnExiting"/>).
|
||||
/// </remarks>
|
||||
private ScheduledDelegate resultsDisplayDelegate;
|
||||
|
||||
/// <summary>
|
||||
/// A task which asynchronously prepares a completed score for display at results.
|
||||
/// This may include performing net requests or importing the score into the database, generally to ensure things are in a sane state for the play session.
|
||||
/// </summary>
|
||||
private Task<ScoreInfo> prepareScoreForDisplayTask;
|
||||
|
||||
/// <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>
|
||||
private void updateCompletionState(bool skipStoryboardOutro = false)
|
||||
private void scoreCompletionChanged(ValueChangedEvent<bool> completed)
|
||||
{
|
||||
// screen may be in the exiting transition phase.
|
||||
// If this player instance is in the middle of an exit, don't attempt any kind of state update.
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
if (!ScoreProcessor.HasCompleted.Value)
|
||||
// Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled.
|
||||
// TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar.
|
||||
// Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
|
||||
// In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
|
||||
// but it still doesn't feel right that this exists here.
|
||||
if (!completed.NewValue)
|
||||
{
|
||||
completionProgressDelegate?.Cancel();
|
||||
completionProgressDelegate = null;
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
resultsDisplayDelegate = null;
|
||||
|
||||
ValidForResume = true;
|
||||
skipOutroOverlay.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (completionProgressDelegate != null)
|
||||
throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once");
|
||||
|
||||
// Only show the completion screen if the player hasn't failed
|
||||
if (HealthProcessor.HasFailed)
|
||||
return;
|
||||
|
||||
// Setting this early in the process means that even if something were to go wrong in the order of events following, there
|
||||
// is no chance that a user could return to the (already completed) Player instance from a child screen.
|
||||
ValidForResume = false;
|
||||
|
||||
// ensure we are not writing to the replay any more, as we are about to consume and store the score.
|
||||
// Ensure we are not writing to the replay any more, as we are about to consume and store the score.
|
||||
DrawableRuleset.SetRecordTarget(null);
|
||||
|
||||
if (!Configuration.ShowResults) return;
|
||||
|
||||
prepareScoreForDisplayTask ??= Task.Run(async () =>
|
||||
{
|
||||
PrepareScoreForResults();
|
||||
|
||||
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();
|
||||
if (!Configuration.ShowResults)
|
||||
return;
|
||||
}
|
||||
|
||||
prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults);
|
||||
|
||||
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
|
||||
|
||||
if (storyboardHasOutro)
|
||||
{
|
||||
// if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending
|
||||
// or the user pressing the skip outro button.
|
||||
skipOutroOverlay.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
|
||||
scheduleCompletion();
|
||||
progressToResults(true);
|
||||
}
|
||||
|
||||
private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
|
||||
/// <summary>
|
||||
/// Asynchronously run score preparation operations (database import, online submission etc.).
|
||||
/// </summary>
|
||||
/// <returns>The final score.</returns>
|
||||
private async Task<ScoreInfo> prepareScoreForResults()
|
||||
{
|
||||
if (!prepareScoreForDisplayTask.IsCompleted)
|
||||
try
|
||||
{
|
||||
scheduleCompletion();
|
||||
return;
|
||||
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, @"Score preparation failed!");
|
||||
}
|
||||
|
||||
// screen may be in the exiting transition phase.
|
||||
if (this.IsCurrentScreen())
|
||||
try
|
||||
{
|
||||
await ImportScore(Score).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, @"Score import failed!");
|
||||
}
|
||||
|
||||
return Score.ScoreInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue the results screen for display.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
|
||||
///
|
||||
/// Calling this method multiple times will have no effect.
|
||||
/// </remarks>
|
||||
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
|
||||
private void progressToResults(bool withDelay)
|
||||
{
|
||||
if (resultsDisplayDelegate != null)
|
||||
// Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be
|
||||
// accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued
|
||||
// may take x00 more milliseconds than expected in the very rare edge case).
|
||||
//
|
||||
// If required we can handle this more correctly by rescheduling here.
|
||||
return;
|
||||
|
||||
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
|
||||
|
||||
resultsDisplayDelegate = new ScheduledDelegate(() =>
|
||||
{
|
||||
if (prepareScoreForDisplayTask?.IsCompleted != true)
|
||||
// If the asynchronous preparation has not completed, keep repeating this delegate.
|
||||
return;
|
||||
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
if (!this.IsCurrentScreen())
|
||||
// This player instance may already be in the process of exiting.
|
||||
return;
|
||||
|
||||
this.Push(CreateResults(prepareScoreForDisplayTask.Result));
|
||||
});
|
||||
}, Time.Current + delay, 50);
|
||||
|
||||
Scheduler.Add(resultsDisplayDelegate);
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
|
||||
|
||||
@ -911,13 +952,6 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
screenSuspension?.Expire();
|
||||
|
||||
if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed)
|
||||
{
|
||||
// proceed to result screen if beatmap already finished playing
|
||||
completionProgressDelegate.RunTask();
|
||||
return true;
|
||||
}
|
||||
|
||||
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
|
||||
// To resolve test failures, forcefully end playing synchronously when this screen exits.
|
||||
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
|
||||
@ -980,7 +1014,13 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
/// <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>
|
||||
protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask;
|
||||
protected virtual Task PrepareScoreForResultsAsync(Score score)
|
||||
{
|
||||
// perform one final population to ensure everything is up-to-date.
|
||||
ScoreProcessor.PopulateScore(score.ScoreInfo);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>.
|
||||
|
Reference in New Issue
Block a user