Closes https://github.com/ppy/osu/issues/25263.
In some circumstances, stable allows skipping twice if a particularly
long storyboarded intro is being displayed:
https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameModes/Play/Player.cs#L1728-L1736
`AllowDoubleSkip` is calculated thus:
3ea48705eb/osu!/GameModes/Play/Player.cs#L1761-L1770
and `leadInTime` is calculated thus:
3ea48705eb/osu!/GameModes/Play/Player.cs#L1342-L1351
The key to watch out for here is `{first,last}EventTime`. `EventManager`
will calculate it on-the-fly as it adds storyboard elements:
3ea48705eb/osu!/GameplayElements/Events/EventManager.cs#L253-L256
However, this pathway is only used for sprite, animation, sample,
and break events. Video and background events use the following pathway:
https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameplayElements/Events/EventManager.cs#L368
Note that this particular overload does not mutate either bound.
Which means that for the purposes of determining where a storyboard
starts and ends temporally, a video event's start time is essentially
ignored.
To reflect that, add a clause that excludes video events from
calculations of `{Earliest,Latest}EventTime`.
stable's `pSpriteText` has a `TextConstantSpacing` flag, that is
selectively enabled for some usages. In particular, these are:
- mania combo counter (not yet implemented)
- taiko combo counter (not yet implemented)
- score counter
- accuracy counter
- scoreboard entries (not yet implemented)
Everything else uses non-fixed-width fonts.
Hilariously, `LegacySpinner` _tried_ to account for this by changing
`Font` to have `fixedWidth: false` specified, only to fail to notice
that `LegacySpriteText` changes `Font` in its BDL, making the property
set do precisely nothing. For this reason, attempting to set `Font`
on a `LegacySpriteText` will now throw.
The import preparation task can actually be non-null when exiting even
if the player hasn't passed:
- fail beatmap
- click import button to import the failed replay
Closes https://github.com/ppy/osu/issues/25247.
The scenario involved here is as follows:
1. User completes beatmap by hitting all notes.
2. `checkScoreCompleted()` determines completion, and calls
`progressToResults(withDelay: true)`.
3. `progressToResults()` schedules `resultsDisplayDelegate`, which
includes a call to `prepareAndImportScoreAsync()`, a second in the
future.
4. User presses quick retry hotkey.
This calls `Player.Restart(quickRestart: true)`,
which invokes `Player.RestartRequested`,
which in turn calls `PlayerLoader.restartRequested(true)`,
which in turn causes `PlayerLoader` to make itself current,
which means that `Player.OnExiting()` will get called.
5. `Player.OnExiting()` sees that `prepareScoreForDisplayTask` is null
(because `prepareAndImportScoreAsync()` - which sets it -
is scheduled to happen in the future), and as such assumes that
the score did not complete. Thus, it marks the score as failed.
6. `Player.Restart()` after invoking `RestartRequested` calls
`PerformExit(false)`, which then will unconditionally call
`prepareAndImportScoreAsync()`. But the score has already been marked
as failed.
The flow above can be described as "convoluted", but I'm not sure I have
it in me right now to try and refactor it again. Therefore, to fix,
switch the `prepareScoreForDisplayTask` null check in
`Player.OnExiting()` to check `GameplayState.HasPassed` instead, as it
is not susceptible to the same out-of-order read issue.