1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-16 09:17:48 +08:00
osu-lazer/osu.Game/Screens/Play/Player.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1290 lines
51 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
2022-06-17 15:37:17 +08:00
#nullable disable
2018-01-21 23:11:43 +08:00
using System;
using System.Diagnostics;
2020-03-24 13:13:46 +08:00
using System.IO;
2018-01-21 23:11:43 +08:00
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
2016-11-09 07:13:20 +08:00
using osu.Framework.Allocation;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables;
using osu.Framework.Extensions;
2016-11-14 16:23:33 +08:00
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
2018-10-02 11:02:47 +08:00
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Extensions;
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;
using osu.Game.Online.API;
2018-06-06 14:10:09 +08:00
using osu.Game.Overlays;
2017-04-18 15:05:58 +08:00
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
2017-04-18 15:05:58 +08:00
using osu.Game.Rulesets.Scoring;
2018-01-21 23:11:43 +08:00
using osu.Game.Rulesets.UI;
2023-12-30 12:41:55 +08:00
using osu.Game.Rulesets.UI.Scrolling;
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;
using osu.Game.Screens.Play.HUD;
2018-01-21 23:11:43 +08:00
using osu.Game.Screens.Ranking;
2018-03-20 15:26:36 +08:00
using osu.Game.Skinning;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Graphics;
2018-04-13 17:19:50 +08:00
2016-11-14 16:23:33 +08:00
namespace osu.Game.Screens.Play
2016-09-29 19:13:58 +08:00
{
2019-11-01 14:32:06 +08:00
[Cached]
public abstract partial class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler, ILocalUserPlayInfo
2016-09-29 19:13:58 +08:00
{
/// <summary>
/// The delay upon completion of the beatmap before displaying the results screen.
/// </summary>
public const double RESULTS_DISPLAY_DELAY = 1000.0;
2021-08-11 17:16:25 +08:00
/// <summary>
/// Raised after <see cref="StartGameplay"/> is called.
/// </summary>
public event Action OnGameplayStarted;
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 bool PlayExitSound => !isRestarting;
protected override UserActivity InitialActivity => new UserActivity.InSoloGame(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
public override bool HideMenuCursorOnNonMouseInput => true;
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? ApplyModTrackAdjustments => 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
public Action<bool> RestartRequested;
2018-04-13 17:19:50 +08:00
private bool isRestarting;
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
public IBindable<bool> LocalUserPlaying => localUserPlaying;
private readonly Bindable<bool> localUserPlaying = new Bindable<bool>();
private readonly Bindable<LocalUserPlayingStates> playingState = new Bindable<LocalUserPlayingStates>();
public int RestartCount;
2018-04-13 17:19:50 +08:00
2022-10-12 14:11:52 +08:00
/// <summary>
/// Whether the <see cref="HUDOverlay"/> is currently visible.
/// </summary>
public IBindable<bool> ShowingOverlayComponents = new Bindable<bool>();
// Should match PlayerLoader for consistency. Cached here for the rare case we push a Player
// without the loading screen (one such usage is the skin editor's scene library).
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
2018-11-29 13:56:29 +08:00
[Resolved]
private ScoreManager scoreManager { get; set; }
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; }
[Resolved]
private OsuGameBase game { get; set; }
2021-10-02 01:22:23 +08:00
public GameplayState GameplayState { get; private set; }
2021-10-02 01:22:23 +08:00
private Ruleset ruleset;
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-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() => GameplayState.Mods.OfType<IApplicableFailOverride>().All(m => m.PerformFail());
2019-09-19 03:49:28 +08:00
public readonly PlayerConfiguration Configuration;
/// <summary>
/// The score for the current play session.
/// Available only after the player is loaded.
/// </summary>
public Score Score { get; private set; }
/// <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 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();
if (!LoadedBeatmapSuccessfully)
return;
PrepareReplay();
2022-06-24 20:25:23 +08:00
ScoreProcessor.NewJudgement += _ => ScoreProcessor.PopulateScore(Score.ScoreInfo);
ScoreProcessor.OnResetFromReplayFrame += () => ScoreProcessor.PopulateScore(Score.ScoreInfo);
gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
2020-03-23 18:31:43 +08:00
}
/// <summary>
/// Run any recording / playback setup for replays.
/// </summary>
protected virtual void PrepareReplay()
{
DrawableRuleset.SetRecordTarget(Score);
}
[BackgroundDependencyLoader(true)]
private void load(OsuConfigManager config, OsuGameBase game, CancellationToken cancellationToken)
2016-10-05 19:49:31 +08:00
{
var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray();
2019-04-09 12:33:16 +08:00
if (gameplayMods.Any(m => m is UnknownMod))
{
Logger.Log("Gameplay was started with an unknown mod applied.", level: LogLevel.Important);
return;
}
if (Beatmap.Value is DummyWorkingBeatmap)
return;
IBeatmap playableBeatmap = loadPlayableBeatmap(gameplayMods, cancellationToken);
if (playableBeatmap == null)
return;
2018-04-13 17:19:50 +08:00
if (!ModUtils.CheckModsBelongToRuleset(ruleset, gameplayMods))
{
Logger.Log($@"Gameplay was started with a mod belonging to a ruleset different than '{ruleset.Description}'.", level: LogLevel.Important);
return;
}
mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
2018-04-13 17:19:50 +08:00
if (game != null)
gameActive.BindTo(game.IsActive);
2021-02-08 19:05:16 +08:00
DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, gameplayMods);
dependencies.CacheAs(DrawableRuleset);
2023-12-30 12:41:55 +08:00
if (DrawableRuleset is IDrawableScrollingRuleset scrollingRuleset)
dependencies.CacheAs(scrollingRuleset.ScrollingInfo);
2021-10-02 01:22:23 +08:00
ScoreProcessor = ruleset.CreateScoreProcessor();
ScoreProcessor.Mods.Value = gameplayMods;
2022-05-30 18:11:54 +08:00
ScoreProcessor.ApplyBeatmap(playableBeatmap);
2018-04-13 17:19:50 +08:00
dependencies.CacheAs(ScoreProcessor);
2023-12-17 18:27:03 +08:00
HealthProcessor = gameplayMods.OfType<IApplicableHealthProcessor>().FirstOrDefault()?.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
HealthProcessor ??= ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
HealthProcessor.ApplyBeatmap(playableBeatmap);
dependencies.CacheAs(HealthProcessor);
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
2018-04-13 17:19:50 +08:00
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
2021-10-05 13:48:10 +08:00
Score = CreateScore(playableBeatmap);
2021-10-05 13:48:10 +08:00
// ensure the score is in a consistent state with the current player.
Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo;
2023-02-07 16:52:47 +08:00
Score.ScoreInfo.BeatmapHash = Beatmap.Value.BeatmapInfo.Hash;
2021-10-05 13:48:10 +08:00
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
Score.ScoreInfo.Mods = gameplayMods;
2021-10-05 13:48:10 +08:00
Implement toggling visibility of pass and fail storyboard layers Closes https://github.com/ppy/osu/issues/6842. This is a rather barebones implementation, just to get this in place somehow at least. The logic is simple - 50% health or above shows pass layer, anything below shows fail layer. This does not match stable logic all across the board because I have no idea how to package that. Stable defines "passing" in like fifty ways: - in mania it's >80% HP (https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameModes/Play/Rulesets/Mania/RulesetMania.cs#L333-L336) - in taiko it's >80% *accuracy* (https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L486-L492) - there's also the part where "geki additions" will unconditionally set passing state (https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameModes/Play/Player.cs#L3561-L3564) - and also the part where at the end of the map, the final passing state is determined by checking whether the user passed more sections than failed (https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameModes/Play/Player.cs#L3320) The biggest issues of these are probably the first two, and they can *probably* be fixed, but would require a new member on `Ruleset` and I'm not sure how to make one look, so I'm not doing that at this time pending collection of ideas on how to do that.
2024-06-14 19:36:40 +08:00
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard));
2021-10-02 01:22:23 +08:00
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
// 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(rulesetSkinProvider);
if (cancellationToken.IsCancellationRequested)
return;
2021-10-15 18:35:08 +08:00
rulesetSkinProvider.AddRange(new Drawable[]
{
failAnimationContainer = new FailAnimationContainer(DrawableRuleset)
2021-10-15 18:35:08 +08:00
{
OnComplete = onFailComplete,
Children = new[]
{
// underlay and gameplay should have access to the skinning sources.
createUnderlayComponents(),
createGameplayComponents(Beatmap.Value)
2021-10-15 18:35:08 +08:00
}
},
FailOverlay = new FailOverlay
2021-10-15 18:35:08 +08:00
{
SaveReplay = async () => await prepareAndImportScoreAsync(true).ConfigureAwait(false),
OnRetry = Configuration.AllowUserInteraction ? () => Restart() : null,
2021-10-15 18:35:08 +08:00
OnQuit = () => PerformExit(true),
},
new HotkeyExitOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
if (PerformExit(false))
// The hotkey overlay dims the screen.
// If the operation succeeds, we want to make sure we stay dimmed to keep continuity.
fadeOut(true);
},
},
});
if (cancellationToken.IsCancellationRequested)
return;
if (Configuration.AllowRestart)
{
rulesetSkinProvider.AddRange(new Drawable[]
{
new HotkeyRetryOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
if (Restart(true))
// The hotkey overlay dims the screen.
// If the operation succeeds, we want to make sure we stay dimmed to keep continuity.
fadeOut(true);
},
},
});
}
dependencies.CacheAs(DrawableRuleset.FrameStableClock);
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
// also give the overlays the ruleset skin provider 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.
failAnimationContainer.Add(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();
});
2022-06-24 20:25:23 +08:00
DrawableRuleset.IsPaused.BindValueChanged(_ =>
2020-10-27 13:10:12 +08:00
{
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
((IBindable<bool>)DrawableRuleset.IsPaused).BindTo(GameplayClockContainer.IsPaused);
2018-04-13 17:19:50 +08:00
DrawableRuleset.NewResult += r =>
{
HealthProcessor.ApplyResult(r);
ScoreProcessor.ApplyResult(r);
2021-10-02 01:22:23 +08:00
GameplayState.ApplyResult(r);
};
DrawableRuleset.RevertResult += r =>
{
HealthProcessor.RevertResult(r);
ScoreProcessor.RevertResult(r);
};
2019-12-11 16:25:06 +08:00
DimmableStoryboard.HasStoryboardEnded.ValueChanged += _ => checkScoreCompleted();
// Bind the judgement processors to ourselves
ScoreProcessor.HasCompleted.BindValueChanged(_ => checkScoreCompleted());
HealthProcessor.Failed += onFail;
2018-04-13 17:19:50 +08:00
2021-08-02 02:14:54 +08:00
// Provide judgement processors to mods after they're loaded so that they're on the gameplay clock,
// this is required for mods that apply transforms to these processors.
ScoreProcessor.OnLoadComplete += _ =>
{
foreach (var mod in gameplayMods.OfType<IApplicableToScoreProcessor>())
mod.ApplyToScoreProcessor(ScoreProcessor);
};
HealthProcessor.OnLoadComplete += _ =>
{
foreach (var mod in gameplayMods.OfType<IApplicableToHealthProcessor>())
mod.ApplyToHealthProcessor(HealthProcessor);
};
2020-10-11 20:46:55 +08:00
IsBreakTime.BindTo(breakTracker.IsBreakTime);
IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
loadLeaderboard();
}
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()
{
var container = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both },
new KiaiGameplayFountains(),
},
};
return container;
}
2018-04-13 17:19:50 +08:00
private Drawable createGameplayComponents(IWorkingBeatmap working) => 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(IWorkingBeatmap working)
{
var container = new Container
2016-10-06 22:33:09 +08:00
{
RelativeSizeAxes = Axes.Both,
2021-10-15 18:35:08 +08:00
Children = new[]
{
2021-10-15 18:35:08 +08:00
DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard)
2021-10-15 18:35:08 +08:00
{
HoldToQuit =
{
2021-10-15 18:35:08 +08:00
Action = () => PerformExit(true),
IsPaused = { BindTarget = GameplayClockContainer.IsPaused },
ReplayLoaded = { BindTarget = DrawableRuleset.HasReplayLoaded },
2021-10-15 18:35:08 +08:00
},
InputCountController =
2021-10-15 18:35:08 +08:00
{
IsCounting =
{
Value = false
},
2021-10-15 18:35:08 +08:00
},
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
Clock = DrawableRuleset.FrameStableClock,
ProcessCustomClock = false,
BreakTracker = breakTracker,
},
// display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
2021-10-15 18:35:08 +08:00
skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime)
{
RequestSkip = performUserRequestedSkip
},
skipOutroOverlay = new SkipOverlay(GameplayState.Storyboard.LatestEventTime ?? 0)
{
2021-10-15 18:35:08 +08:00
RequestSkip = () => progressToResults(false),
Alpha = 0
},
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
2021-10-15 18:35:08 +08:00
PauseOverlay = new PauseOverlay
{
OnResume = Resume,
Retries = RestartCount,
OnRetry = () => Restart(),
OnQuit = () => PerformExit(true),
2021-10-15 18:35:08 +08:00
},
},
};
if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays)
{
skipIntroOverlay.Expire();
skipOutroOverlay.Expire();
}
return container;
}
2018-04-13 17:19:50 +08:00
private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime)
{
updateGameplayState();
updatePauseOnFocusLostState();
HUDOverlay.InputCountController.IsCounting.Value = !isBreakTime.NewValue;
}
private void updateGameplayState()
2020-08-04 03:25:45 +08:00
{
2024-10-01 16:41:59 +08:00
bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value;
bool inBreak = breakTracker.IsBreakTime.Value || DrawableRuleset.IsPaused.Value || GameplayState.HasFailed;
if (inGameplay)
playingState.Value = inBreak ? LocalUserPlayingStates.Break : LocalUserPlayingStates.Playing;
else
playingState.Value = LocalUserPlayingStates.NotPlaying;
2024-10-01 16:41:59 +08:00
localUserPlaying.Value = playingState.Value == LocalUserPlayingStates.Playing;
OverlayActivationMode.Value = playingState.Value == LocalUserPlayingStates.Playing ? OverlayActivation.Disabled : OverlayActivation.UserTriggered;
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.IsPaused.Value;
2020-10-27 13:10:12 +08:00
}
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(Mod[] gameplayMods, CancellationToken cancellationToken)
{
IBeatmap playable;
try
{
if (Beatmap.Value.Beatmap == null)
throw new InvalidOperationException("Beatmap was not loaded");
2018-04-13 17:19:50 +08:00
var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset;
2021-10-02 01:22:23 +08:00
ruleset = rulesetInfo.CreateInstance();
if (ruleset == null)
throw new RulesetLoadException("Instantiation failure");
try
{
playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, gameplayMods, cancellationToken);
}
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;
2021-10-02 01:22:23 +08:00
ruleset = rulesetInfo.CreateInstance();
playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, gameplayMods, cancellationToken);
2017-04-06 14:34:52 +08:00
}
2018-04-13 17:19:50 +08:00
if (playable.HitObjects.Count == 0)
{
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Important);
return null;
}
}
catch (OperationCanceledException)
{
// Load has been cancelled. No logging is required.
return null;
}
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap successfully!");
//couldn't load, hard abort!
return null;
}
return playable;
}
2018-07-19 00:18:07 +08:00
/// <summary>
/// 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>
2021-02-09 16:14:16 +08:00
/// <param name="showDialogFirst">
/// Whether the pause or fail dialog should be shown before performing an exit.
2021-06-17 22:04:58 +08:00
/// If <see langword="true"/> and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead.
/// </param>
/// <returns>Whether this call resulted in a final exit.</returns>
protected bool PerformExit(bool showDialogFirst)
{
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 && GameplayState.HasFailed)
{
failAnimationContainer.FinishTransforms(true);
return false;
}
// 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.
// this may fail (see internal checks in Pause()) but the fail cases are temporary, so don't fall through to Exit().
Pause();
return false;
}
}
// Matching osu!stable behaviour, if the results screen is pending and the user requests an exit,
// show the results instead.
if (GameplayState.HasPassed && !isRestarting)
{
progressToResults(false);
return false;
}
2023-02-06 14:59:37 +08:00
// import current score if possible.
prepareAndImportScoreAsync();
// Screen may not be current if a restart has been performed.
if (this.IsCurrentScreen())
{
// 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();
}
return true;
}
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();
}
2021-05-17 17:41:56 +08:00
/// <summary>
/// Seek to a specific time in gameplay.
/// </summary>
/// <param name="time">The destination time to seek to.</param>
public void Seek(double time) => GameplayClockContainer.Seek(time);
private ScheduledDelegate frameStablePlaybackResetDelegate;
/// <summary>
/// Specify and seek to a custom start time from which gameplay should be observed.
/// </summary>
/// <remarks>
/// This performs a non-frame-stable seek. Intermediate hitobject judgements may not be applied or reverted correctly during this seek.
/// </remarks>
/// <param name="time">The destination time to seek to.</param>
protected void SetGameplayStartTime(double time)
{
if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed)
frameStablePlaybackResetDelegate.RunTask();
bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
DrawableRuleset.FrameStablePlayback = false;
GameplayClockContainer.Reset(time);
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
}
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>
/// <param name="quickRestart">Whether a quick restart was requested (skipping intro etc.).</param>
2023-10-12 18:06:19 +08:00
/// <returns>Whether this call resulted in a restart.</returns>
public bool Restart(bool quickRestart = false)
{
if (!Configuration.AllowRestart)
return false;
isRestarting = true;
// 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();
RestartRequested?.Invoke(quickRestart);
2019-11-01 14:32:06 +08:00
return PerformExit(false);
}
2018-04-13 17:19:50 +08:00
/// <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;
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>
private void checkScoreCompleted()
2016-11-29 22:59:56 +08:00
{
// If this player instance is in the middle of an exit, don't attempt any kind of state update.
if (!this.IsCurrentScreen())
return;
// Handle cases of arriving at this method when not in a completed state.
// - When a storyboard completion triggered this call earlier than gameplay finishes.
// - When a replay has been rewound before a queued resultsDisplayDelegate has run.
//
// Currently, even if this scenario is hit, prepareAndImportScoreAsync has already been queued (and potentially run).
// In the scenarios above, this is a non-issue, but it still feels a bit convoluted to have to cancel in this method.
// Maybe this can be improved with further refactoring.
if (!ScoreProcessor.HasCompleted.Value)
{
resultsDisplayDelegate?.Cancel();
resultsDisplayDelegate = null;
GameplayState.HasPassed = false;
ValidForResume = true;
skipOutroOverlay.Hide();
return;
}
// Only show the completion screen if the player hasn't failed
if (GameplayState.HasFailed)
return;
2018-04-13 17:19:50 +08:00
GameplayState.HasPassed = true;
// 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;
2018-04-13 17:19:50 +08:00
bool storyboardStillRunning = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
// If the current beatmap has a storyboard, this method will be called again on storyboard completion.
// Alternatively, the user may press the outro skip button, forcing immediate display of the results screen.
if (storyboardStillRunning)
{
skipOutroOverlay.Show();
return;
}
progressToResults(true);
2016-11-29 22:59:56 +08:00
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Queue the results screen for display.
/// </summary>
/// <remarks>
2021-06-18 15:18:20 +08:00
/// 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"/>.
/// </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 (!Configuration.ShowResults)
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;
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
resultsDisplayDelegate?.Cancel();
resultsDisplayDelegate = new ScheduledDelegate(() =>
{
if (prepareScoreForDisplayTask == null)
{
2023-02-06 14:59:37 +08:00
// Try importing score since the task hasn't been invoked yet.
prepareAndImportScoreAsync();
return;
}
if (!prepareScoreForDisplayTask.IsCompleted)
2021-06-18 15:18:20 +08:00
// If the asynchronous preparation has not completed, keep repeating this delegate.
return;
resultsDisplayDelegate?.Cancel();
if (prepareScoreForDisplayTask.GetResultSafely() == null)
{
// If score import did not occur, we do not want to show the results screen.
return;
}
if (!this.IsCurrentScreen())
// This player instance may already be in the process of exiting.
return;
2022-01-06 21:54:43 +08:00
this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely()));
}, Time.Current + delay, 50);
Scheduler.Add(resultsDisplayDelegate);
2016-11-29 22:59:56 +08:00
}
2023-02-06 14:59:37 +08:00
/// <summary>
/// Asynchronously run score preparation operations (database import, online submission etc.).
2023-02-06 14:59:37 +08:00
/// </summary>
/// <param name="forceImport">Whether the score should be imported even if non-passing (or the current configuration doesn't allow for it).</param>
/// <returns>The final score.</returns>
[ItemCanBeNull]
private Task<ScoreInfo> prepareAndImportScoreAsync(bool forceImport = false)
{
2023-02-06 14:59:37 +08:00
// Ensure we are not writing to the replay any more, as we are about to consume and store the score.
DrawableRuleset.SetRecordTarget(null);
if (prepareScoreForDisplayTask != null)
return prepareScoreForDisplayTask;
// We do not want to import the score in cases where we don't show results
bool canShowResults = Configuration.ShowResults && ScoreProcessor.HasCompleted.Value && GameplayState.HasPassed;
if (!canShowResults && !forceImport)
return Task.FromResult<ScoreInfo>(null);
2023-02-06 14:59:37 +08:00
// Clone score before beginning any async processing.
// - Must be run synchronously as the score may potentially be mutated in the background.
// - Must be cloned for the same reason.
Score scoreCopy = Score.DeepClone();
return prepareScoreForDisplayTask = Task.Run(async () =>
{
try
{
await PrepareScoreForResultsAsync(scoreCopy).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score preparation failed!");
}
try
{
await ImportScore(scoreCopy).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, @"Score import failed!");
}
return scoreCopy.ScoreInfo;
});
}
protected override bool OnScroll(ScrollEvent e)
{
// During pause, allow global volume adjust regardless of settings.
if (GameplayClockContainer.IsPaused.Value)
return false;
// Block global volume adjust if the user has asked for it (special case when holding "Alt").
return mouseWheelDisabled.Value && !e.AltPressed;
}
2019-03-18 10:48:11 +08:00
#region Gameplay leaderboard
protected readonly Bindable<bool> LeaderboardExpandedState = new BindableBool();
private void loadLeaderboard()
{
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
var gameplayLeaderboard = CreateGameplayLeaderboard();
if (gameplayLeaderboard != null)
{
LoadComponentAsync(gameplayLeaderboard, leaderboard =>
{
if (!LoadedBeatmapSuccessfully)
return;
leaderboard.Expanded.BindTo(LeaderboardExpandedState);
AddLeaderboardToHUD(leaderboard);
});
}
}
[CanBeNull]
protected virtual GameplayLeaderboard CreateGameplayLeaderboard() => null;
protected virtual void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) => HUDOverlay.LeaderboardFlow.Add(leaderboard);
private void updateLeaderboardExpandedState() =>
LeaderboardExpandedState.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
#endregion
2019-03-18 10:48:11 +08:00
#region Fail Logic
2023-11-23 09:01:59 +08:00
/// <summary>
/// Invoked when gameplay has permanently failed.
/// </summary>
protected virtual void OnFail()
{
}
2019-03-18 10:48:11 +08:00
protected FailOverlay FailOverlay { get; private set; }
private FailAnimationContainer failAnimationContainer;
2019-06-04 15:13:16 +08:00
2017-08-05 10:59:58 +08:00
private bool onFail()
2017-01-20 15:51:43 +08:00
{
// Failing after the quit sequence has started may cause weird side effects with the fail animation / effects.
if (GameplayState.HasQuit)
return false;
if (!CheckModsAllowFailure())
2017-08-05 10:59:58 +08:00
return false;
2018-04-13 17:19:50 +08:00
if (Configuration.AllowFailAnimation)
{
Debug.Assert(!GameplayState.HasFailed);
Debug.Assert(!GameplayState.HasPassed);
Debug.Assert(!GameplayState.HasQuit);
GameplayState.HasFailed = true;
updateGameplayState();
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)
PauseOverlay.Hide();
failAnimationContainer.Start();
// Failures can be triggered either by a judgement, or by a mod.
//
// For the case of a judgement, due to ordering considerations, ScoreProcessor will not have received
// the final judgement which triggered the failure yet (see DrawableRuleset.NewResult handling above).
//
// A schedule here ensures that any lingering judgements from the current frame are applied before we
// finalise the score as "failed".
Schedule(() =>
{
ScoreProcessor.FailScore(Score.ScoreInfo);
OnFail();
if (GameplayState.Mods.OfType<IApplicableFailOverride>().Any(m => m.RestartOnFail))
Restart(true);
});
}
else
{
ScoreProcessor.FailScore(Score.ScoreInfo);
}
2018-10-14 23:18:52 +08:00
2017-08-05 10:59:58 +08:00
return true;
2017-01-20 15:51:43 +08:00
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Invoked when the fail animation has finished.
/// </summary>
2019-06-04 15:13:16 +08:00
private void onFailComplete()
{
GameplayClockContainer.Stop();
2019-03-18 10:48:11 +08:00
FailOverlay.Retries = RestartCount;
FailOverlay.Show();
2017-01-20 15:51:43 +08:00
}
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>
protected virtual double PauseCooldownDuration => 1000;
2019-03-18 10:48:11 +08:00
protected PauseOverlay PauseOverlay { get; private set; }
private double? lastPauseActionTime;
protected bool PauseCooldownActive =>
lastPauseActionTime.HasValue && GameplayClockContainer.CurrentTime < lastPauseActionTime + PauseCooldownDuration;
2021-02-19 14:34:39 +08:00
/// <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
&& !GameplayState.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
&& !GameplayState.HasFailed
2019-03-18 10:48:11 +08:00
// 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.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
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
2018-04-13 17:19:50 +08:00
if (!LoadedBeatmapSuccessfully)
2017-07-19 18:10:04 +08:00
return;
2018-04-13 17:19:50 +08:00
2019-01-23 19:52:00 +08:00
Alpha = 0;
this
2017-07-16 23:28:20 +08:00
.ScaleTo(0.7f)
2017-07-23 02:50:25 +08:00
.ScaleTo(1, 750, Easing.OutQuint)
2017-07-16 23:28:20 +08:00
.Delay(250)
.FadeIn(250);
2018-04-13 17:19:50 +08:00
ApplyToBackground(b =>
{
b.IgnoreUserSettings.Value = false;
b.BlurAmount.Value = 0;
b.FadeColour(Color4.White, 250);
// bind component bindables.
((IBindable<bool>)b.IsBreakTime).BindTo(breakTracker.IsBreakTime);
b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
failAnimationContainer.Background = b;
});
2018-04-13 17:19:50 +08:00
HUDOverlay.IsPlaying.BindTo(localUserPlaying);
2022-10-12 14:11:52 +08:00
ShowingOverlayComponents.BindTo(HUDOverlay.ShowHud);
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
2019-12-11 04:06:13 +08:00
storyboardReplacesBackground.Value = GameplayState.Storyboard.ReplacesBackground && GameplayState.Storyboard.HasDrawable;
foreach (var mod in GameplayState.Mods.OfType<IApplicableToPlayer>())
mod.ApplyToPlayer(this);
foreach (var mod in GameplayState.Mods.OfType<IApplicableToHUD>())
2019-06-29 09:23:59 +08:00
mod.ApplyToHUD(HUDOverlay);
foreach (var mod in GameplayState.Mods.OfType<IApplicableToTrack>())
mod.ApplyToTrack(GameplayClockContainer.AdjustmentsFromMods);
2020-09-04 03:56:47 +08:00
updateGameplayState();
GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint);
2021-08-11 17:16:25 +08:00
StartGameplay();
2021-08-11 17:16:25 +08:00
OnGameplayStarted?.Invoke();
}
/// <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.IsRunning)
Logger.Error(new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"), "Clock failure");
GameplayClockContainer.Reset(startClock: true);
if (Configuration.AutomaticallySkipIntro)
skipIntroOverlay.SkipWhenReady();
}
2018-04-13 17:19:50 +08:00
public override void OnSuspending(ScreenTransitionEvent e)
{
screenSuspension?.RemoveAndDisposeImmediately();
fadeOut();
base.OnSuspending(e);
}
2018-04-13 17:19:50 +08:00
public override bool OnExiting(ScreenExitEvent e)
2016-12-17 00:13:24 +08:00
{
screenSuspension?.RemoveAndDisposeImmediately();
// Eagerly clean these up as disposal of child components is asynchronous and may leave sounds playing beyond user expectations.
failAnimationContainer?.Stop();
2023-02-06 18:31:45 +08:00
PauseOverlay?.StopAllSamples();
2023-10-27 19:27:48 +08:00
if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed)
{
Debug.Assert(resultsDisplayDelegate == null);
2023-10-27 19:27:48 +08:00
if (!GameplayState.HasFailed)
GameplayState.HasQuit = true;
2023-10-27 19:27:48 +08:00
if (DrawableRuleset.ReplayScore == null)
ScoreProcessor.FailScore(Score.ScoreInfo);
}
// 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(e);
2016-12-17 00:13:24 +08:00
}
2018-04-13 17:19:50 +08:00
2020-12-18 16:47:33 +08:00
/// <summary>
/// Creates the player's <see cref="Scoring.Score"/>.
2020-12-18 16:47:33 +08:00
/// </summary>
2021-10-05 13:48:10 +08:00
/// <param name="beatmap"></param>
/// <returns>The <see cref="Scoring.Score"/>.</returns>
2021-10-05 13:48:10 +08:00
protected virtual Score CreateScore(IBeatmap beatmap) => new Score
{
ScoreInfo = new ScoreInfo
{
User = api.LocalUser.Value,
2023-12-21 19:56:19 +08:00
ClientVersion = game.Version,
},
};
2020-12-18 14:36:24 +08:00
2020-12-18 16:47:33 +08:00
/// <summary>
/// Imports the player's <see cref="Scoring.Score"/> to the local database.
2020-12-18 16:47:33 +08:00
/// </summary>
/// <param name="score">The <see cref="Scoring.Score"/> to import.</param>
/// <returns>The imported score.</returns>
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)
return Task.CompletedTask;
ByteArrayArchiveReader replayReader = null;
if (score.ScoreInfo.Ruleset.IsLegacyRuleset())
{
using (var stream = new MemoryStream())
{
new LegacyScoreEncoder(score, GameplayState.Beatmap).Encode(stream);
replayReader = new ByteArrayArchiveReader(stream.ToArray(), "replay.osr");
}
}
// the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import.
var importableScore = score.ScoreInfo.DeepClone();
var imported = scoreManager.Import(importableScore, replayReader);
2024-04-23 02:23:38 +08:00
Debug.Assert(imported != null);
imported.PerformRead(s =>
{
// because of the clone above, it's required that we copy back the post-import hash/ID to use for availability matching.
score.ScoreInfo.Hash = s.Hash;
score.ScoreInfo.ID = s.ID;
score.ScoreInfo.Files.AddRange(s.Files.Detach());
});
return Task.CompletedTask;
}
/// <summary>
/// Prepare the <see cref="Scoring.Score"/> for display at results.
/// </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;
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>
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score)
{
AllowRetry = true,
ShowUserStatistics = true,
};
2020-12-18 14:36:24 +08:00
private void fadeOut(bool instant = false)
2017-09-14 20:28:53 +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
if (this.IsCurrentScreen())
{
ApplyToBackground(b =>
{
b.IgnoreUserSettings.Value = true;
// May be null if the load never completed.
if (breakTracker != null)
{
b.IsBreakTime.UnbindFrom(breakTracker.IsBreakTime);
b.IsBreakTime.Value = false;
}
});
storyboardReplacesBackground.Value = false;
}
2017-09-14 20:28:53 +08:00
}
2018-04-13 17:19:50 +08:00
2019-03-18 10:48:11 +08:00
#endregion
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
public IBindable<LocalUserPlayingStates> PlayingState => playingState;
2016-09-29 19:13:58 +08:00
}
2017-04-11 23:09:45 +08:00
}