I don't know how to reproduce this:
```
[runtime] 2026-04-15 05:43:26 [verbose]: 📺 OsuScreenStack#478(depth:4) exit from ScreenQueue#281
[runtime] 2026-04-15 05:43:26 [verbose]: 📺 OsuScreenStack#478(depth:4) resume to ScreenIntro#687
[runtime] 2026-04-15 05:43:26 [verbose]: 📺 BackgroundScreenStack#692(depth:1) exit from MatchmakingBackgroundScreen#393
[runtime] 2026-04-15 05:43:26 [verbose]: 📺 BackgroundScreenStack#692(depth:1) resume to BackgroundScreenDefault#210
[runtime] 2026-04-15 05:43:26 [verbose]: 📺 OsuScreenStack#478(depth:3) exit from ScreenIntro#687
[runtime] 2026-04-15 05:43:26 [verbose]: 📺 OsuScreenStack#478(depth:3) resume to MainMenu#505
[runtime] 2026-04-15 05:43:26 [verbose]: 🌅 Global background change queued
[runtime] 2026-04-15 05:43:26 [verbose]: ButtonSystem's state changed from EnteringMode to TopLevel
[runtime] 2026-04-15 05:43:26 [debug]: Focus changed from nothing to DialogOverlay.
[runtime] 2026-04-15 05:43:26 [error]: An unhandled error has occurred.
[runtime] 2026-04-15 05:43:26 [error]: System.NullReferenceException: Object reference not set to an instance of an object.
[runtime] 2026-04-15 05:43:26 [error]: at osu.Game.Screens.OnlinePlay.Matchmaking.Queue.RatingDistributionGraph.get_TooltipContent() in /home/smgi/Repos/osu/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/RatingDistributionGraph.cs:line 434
[runtime] 2026-04-15 05:43:26 [error]: at osu.Framework.Graphics.Cursor.IHasCustomTooltip`1.osu.Framework.Graphics.Cursor.IHasCustomTooltip.get_TooltipContent()
[runtime] 2026-04-15 05:43:26 [error]: at osu.Framework.Graphics.Cursor.TooltipContainer.hasValidTooltip(ITooltipContentProvider target)
[runtime] 2026-04-15 05:43:26 [error]: at osu.Framework.Graphics.Cursor.TooltipContainer.Update()
[runtime] 2026-04-15 05:43:26 [error]: at osu.Framework.Graphics.Drawable.UpdateSubTree()
```
As unplausible as it may seem, the only thing that can be null here is
`GetContainingInputManager()`. The point of this exercise is to simply
remove it by relying on `OnMouseMove` events instead.
The panels look up the online APIUser models. Maybe I could do this by
doing the lookups async inside `RankedPlayMatchPanel`, but this will
probably do for now?
I was kind of lazy with the disappear/appear stuff. Made it properly set
the required state at the correct time now.
Made a second fix to change it into a visibility container, so that
bindable states are deduped. On `master` it would re-appear from the
bottom with every stage change.
This commit adds an always present overlay to all ranked play screens,
meant to indicate to the user that a ranked play session in currently in
progress.
This has been largely inspired by the pre-shader argon healthbar code.
It shouldn't have the same performance concerns, however, since the
paths are only calculated once when loading the drawable (and eventually
when it is resized, if ever).
`RankedPlayScreen`:
<img width="1838" height="1353" alt="image"
src="https://github.com/user-attachments/assets/c621d759-a88f-49f0-b0df-3a6da90eca65"
/>
Gameplay:
<img width="1838" height="1353" alt="image"
src="https://github.com/user-attachments/assets/ee848d06-3878-4cbb-bd4b-de2c4bcfa688"
/>
Currently the drawable overlaps with some components, but it will be
resolved in later pull requests.
---------
Co-authored-by: Dean Herbert <pe@ppy.sh>
- [x] Depends on https://github.com/ppy/osu/pull/37226
- [x] Depends on https://github.com/ppy/osu-server-spectator/pull/464
This adds two new components to the queue screen:
- A listing of the most recently completed matches (global).
- A rank distribution graph.
It looks something like this (fake data / test scene):
<img width="1669" height="1005" alt="image"
src="https://github.com/user-attachments/assets/caa57119-4267-4c6e-9898-2f414de865bf"
/>
It's completely dev-design(TM), but I used Lichess as inspiration for
the graph, and the original design document as inspiration for the
panels.
As for the history, because these are _completed_ matches one of the
player life points will always be 0, but I've designed it so as to
possibly support showing ongoing matches too in the future. It's only
supported for ranked play right now, though there is no reason we
couldn't track quick play rooms too (it's just... I'm not sure how to
design the panels for quick play).
---------
Co-authored-by: Dean Herbert <pe@ppy.sh>
The main goal here is to:
- Make keyboard selection play previews just like hovering does with
mouse.
- Make sure cards don't play previews when they are in animation.
- Drive by fix to fix toggling discard not working via keyboard.
(I really want to rewrite all these classes, the structure is not great)
---------
Co-authored-by: Dan Balasescu <smoogipoo@smgi.me>
Closes https://github.com/ppy/osu/issues/37232.
The actual fix is
https://github.com/ppy/osu/commit/e959b20517497a093d3c00a17457c5d36bf57651;
everything else is window dressing / test harness to ensure I don't try
and do a wrong change like https://github.com/ppy/osu/pull/37251 did. I
recommend reviewing commit-by-commit.
See [this desmos](https://www.desmos.com/calculator/a5yjpacvxa) for
visual explanation of change, I think it does a better job at explaining
this than any words I could type here.
Of note:
- In the end this did only affect 14K but that should never be assumed
when floating point is involved.
- Test cases generated here were generated in stable manually.
- Except for 11 / 13 / 15 / 17K which are not officially supported and
which don't work in lazer due to orthogonal reasons (see comment added
in this PR in `ManiaBeatmapConverter`), decoding in lazer was always
fine.
- My worry was that the old encoding method before this PR could
potentially cause stable to move a note from one column to another but
thankfully that is not the case. The old method of encoding columns as X
positions does not cause issues wherein lazer reads them back
differently than stable after encode.
I checked this by checking out `master`, re-encoding all of the test
stair-pattern nK beatmaps added in this PR on `master`, exporting that
as compatibility, re-importing to stable, and cross-checking that the
decoded beatmap is visually the same on lazer and on stable.
This is important to check because if this wasn't the case, we'd
potentially have cases of actual online beatmaps (remember that we have
BSS now) wherein a beatmap plays differently on stable than on lazer due
to notes moving between columns, and would need to screen for this being
the case and potentially apply corrective / reconciliatory action.
Just the bare minimum code quality so I can start working on these
classes..
Please push back if this doesn't seem better than what was already
there. This is mostly autopilot fixing for me based on how I've been
writing code for osu! to date.
There are changes to the load process but nothing which should cause
issues, I hope.
Just an initial grab bag to keep these PRs small.
### Avoid showing countdown update when at discard screen
This is needless. We already have the `DiscardFinish` stage which has a
short countdown. Playing this change to the user creates unnecessary
confusion.
### Allow stage caption text to be changed at any point
Also remove custom colour support. We'll handle this internally in a
better way in the future.
### Better explain why we're waiting after discarding our own cards
RFC
Until now, if the initial `BeginPlaySession()` call failed, the client
would continue operating as if it didn't - it would still continue to
send frames and call `EndPlaySession()` at the end of a session.
Server-side, two things generally can happen after this:
- The sent frames and the `EndPlaySession()` call are
[completely](https://github.com/ppy/osu-server-spectator/blob/7bab117e9d161455485368f63a0607a9e53f9f8a/osu.Server.Spectator/Hubs/Spectator/SpectatorHub.cs#L122-L125)
[ignored](https://github.com/ppy/osu-server-spectator/blob/7bab117e9d161455485368f63a0607a9e53f9f8a/osu.Server.Spectator/Hubs/Spectator/SpectatorHub.cs#L153-L157)
as no-ops, or
- A hub filter (like `ClientVersionChecker`) that failed the initial
`BeginPlaySession()` call continues to fail the calls to
`SendFrameData()` and `EndPlaySession()`, all the while creating a storm
in logs, because it needs to throw `HubException`s to communicate to
users that they need to update their game, and the exceptions can't be
silenced from logs because they look like every other failure.
To that end, this has two goals: reduce useless network traffic, and
reduce noise in spectator server logs after the client version checks
were recently reactivated.
Probably needs tests, but unsure if everyone's going to be on board with
this to begin with to be quite frank, so I'm leaving tests for when I'm
told this needs tests.
1. Gives `MatchmakingJoinLobby` parameters.
2. Adds additional data to lobby status update models.
A further PR will build upon (2) to add more data to the queue screen.
In human words: I read [this forum
thread](https://osu.ppy.sh/community/forums/topics/2195138?n=1) today
and was horrified to see the user there opening the F3 options menu in
song select *when the F1 mod overlay was pulled up*, which is (a) not
intended UX, (b) looks terrible, and (c) just wrecks the game
behaviourally wholesale from start to end.
So with this change you don't get to open options via F3 while inside
mod overlay at all.
The `Action` shadowing is pretty ugly but I don't have better ideas.
Initially I tried to mess with `Enabled` (as I did once previously, see
https://github.com/ppy/osu/commit/36628e24f92c286b87c118c7c1bb9bc582895571),
but it's much more complicated in this case because the enabled state
needs to be restored when the buttons reappear, or it could change
independently while the buttons are temporarily hidden, etc. So I'd
rather just not deal with all that and invent a parallel scheme.
- No longer holds realm write transaction open while performing sqlite
lookups.
- No longer attempts a write transaction when it will be a noop.
I'll admit that this is maybe working around the actual realm write part
being slow, but as I can't profile the issue locally, the sluggishness
may actually be in sqlite for those users affected (since it's only been
reported for tag population and not difficulty calculation?).
Regardless, this should fix the issue this iteration.
I also adjusted the user messaging to let them know why tag population
is happening, since we've had some questions as to why it's running in
the first place (it only happens once a month, so that's
understandable).
- [x] Depends on https://github.com/ppy/osu/pull/37227.
- Closes https://github.com/ppy/osu/issues/34699.
- Closes https://github.com/ppy/osu/issues/37210.
Note that https://github.com/ppy/osu/pull/36128 also exists and has
valid improvements which can be addressed separately. This is intended
to be something we can act on immediately.
---------
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
This was a private request for roundtable event usage. It’s also a
common feature request, so I decided to spend a bit of time getting this
working well-enough.
https://github.com/user-attachments/assets/acceb57f-2979-43d0-9fc2-33e977bd2dd5
---
### Delay loading spinner / loading layer initial load briefly to avoid
flickering
There's cases in this overlay where loading takes a few milliseconds.
The loading spinner gets annoying. This also happens elsewhere, so this
could be considered a global fix. Separate PR? probably...
### Ingest loading state of dashboard child content to show more correct
loading layer
Each display had their own loading layer implementation, but this is
already too deep (inside the scroll content) and doesn't display great
when for instance, results don't take up the full screen height.
---------
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
This was used in one place, but I foresee this being a more common
scenario. This also fixes an edge case where the dismiss process would
fail if completion happened on an async thread before the
`NotificationOverlay`'s scheduler could handle the initial ingress.
`RankedPlaySubScreen.CenterColumn` had a padding which moved the card
hand up slightly, causing it to not fully dissapear when contracting.
The padding doesn't serve any purpose anymore (remnant of the very early
versions of the screens), so I just removed it.
I checked against `DiscardScreen`, `PickScreen`, `OpponentPickScreen` &
`EndedScreen` to make sure this doesn't cause any layout breakage.
Also removed the `ButtonsContainer` since it isn't being used anywhere
anymore.
https://github.com/user-attachments/assets/2fd32407-fbf7-45a3-b92a-0730a0f8a3fd
Closes https://github.com/ppy/osu/issues/37185.
The checkboxes in the context menu's ternary states were supposed to
always show the origin "in local space", even if anchor is set to
"closest". The issue here was reusing a method that only really made
sense with closest anchor active for explicit application of
"local-space" origin.
To recap:
- Below I will use concepts of "local-space origin" and "screen-space
origin". To understand the difference, let's use an example:
Say there's a drawable with 180 degree rotation. Suppose it has the
"local origin" of `TopCentre`. The "local origin" is just the `Drawable`
notion of origin; you'd literally set `d.Origin = Anchor.TopCentre`.
The "screen-space origin" of this drawable is `BottomCentre`, because
due to the rotation, that's how the component will visually behave when
its position is altered.
The same sort of distinction applies forth to things like flips /
negative scale and such.
- When you have closest anchor selected, you can only choose the anchor
to snap to. The drawable will snap to that anchor, and choose an origin
closest to it *in screen space* such that the "closest" in "closest
anchor" works as users would expect it to. In this state, if you open
the context menu for origin, all items will be disabled, but the ternary
menu items will show the origin state *as translated back to local
space*.
- When you have an explicit anchor selected, you can choose both the
anchor and origin. In that case, the origin picked is always picked in
local space.
In the end, this is all consistent with how the `Origin` property on
`Drawable` works, and also with what is serialised to skin jsons.
## [Rewrite `BackgroundMusicManager` to not run into framework
breakage](https://github.com/ppy/osu/commit/622216d8911832c39fa4e126b2810e4e0f46cbf7)
The attempted proper fix to this was
https://github.com/ppy/osu-framework/pull/6727. Unfortunately when
presented with [the framework
bump](https://github.com/ppy/osu/pull/37217) with that change, CI says
"you're stupid" and fails on some disposal idiocy that of course is
undebuggable and irreproducible:
The active test run was aborted. Reason: Test host process crashed :
Unhandled exception. System.AggregateException: One or more errors
occurred. (Object reference not set to an instance of an object.)
---> System.NullReferenceException: Object reference not set to an
instance of an object.
at osu.Framework.Audio.Sample.SampleChannelBass.Dispose(Boolean
disposing)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext
executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunInternal(ExecutionContext
executionContext, ContextCallback callback, Object state)
at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task&
currentTaskSlot, Thread threadPoolThread)
--- End of inner exception stack trace ---
at osu.Framework.Audio.AudioCollectionManager`1.UpdateChildren()
at osu.Framework.Audio.AudioCollectionManager`1.UpdateChildren()
at osu.Framework.Audio.AudioCollectionManager`1.UpdateChildren()
at osu.Framework.Audio.AudioCollectionManager`1.UpdateChildren()
at osu.Framework.Threading.AudioThread.OnExit()
at osu.Framework.Threading.GameThread.setExitState(GameThreadState
exitState)
at osu.Framework.Threading.GameThread.RunSingleFrame()
at osu.Framework.Threading.GameThread.<createThread>g__runWork|70_0()
at System.Threading.ExecutionContext.RunInternal(ExecutionContext
executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunInternal(ExecutionContext
executionContext, ContextCallback callback, Object state)
(https://github.com/ppy/osu/actions/runs/24019928154/job/70046733058?pr=37217#step:5:119)
I no longer have the energy for any of this shit.
@nekodex would appreciate if you could check that I actually haven't
broken anything with the bgm here. Seems okay to me in test scenes at
least.
## [Apply lowest-effort maybe-fixing changes to a bunch of flaking
tests](https://github.com/ppy/osu/commit/7bd3ca4adfcce5b90add11565a13f3fe9177ad5e)
None of the failures are reproducible locally, of course. I'm tired of
this. If anyone else wants to subject themselves to actually
investigating any of these, by all means, godspeed and good luck.
Displays the stage name and details like currently picking player
and damage multiplayer where applicable.
Currently only shown on the discard and pick stages.
# Move user retrieval to `RankedPlayScreen`
This is done because I need the relevant `APIUser` instances in order to
pass them to the overlay component. `RankedPlayScreen` seems like the
appropriate place to manage the overlays since it manages the stage
subscreens, hence the need to access the `APIUser`s in here.
# Add current stage overlay to ranked play
The actual change of this PR. Very much a dev design.
https://github.com/user-attachments/assets/2388e934-2fc7-4e15-9947-9f98412765d2
---------
Co-authored-by: Dean Herbert <pe@ppy.sh>
This has gone through a few iterations, and eventually ended up as a
simple text percentage display next to the username. I feel that adding
another progress bar right next to the big healthbar would make things
too cluttered, and trying to move the beatmap state elsewhere would make
it too disconnected from the players that are potentially downloading a
beatmap.
I considered making the local user fetch download progress data using
`BeatmapDownloadTracker` instead of relying on `BeatmapAvailability` in
order to get more frequent updates, but that would add a lot of extra
complexity for little gain IMO.
[Screencast_20260403_095644.webm](https://github.com/user-attachments/assets/85fbd4b8-6b5c-41d2-b29b-c93885f73bb3)
Kinda self explanatory, adds a second client configurations so its
easier to test multiplayer-specific things when using VSCode
For people that don't know how to use this, basically just run the first
debug like normal, then swap to the second client option and run that.
You can also do it in reverse. Visual guide here:
https://github.com/user-attachments/assets/1dab50eb-3bd2-422d-a776-852ac4454213
Example:
https://github.com/ppy/osu/actions/runs/23900675414/job/69696255970?pr=37178#step:5:38
Regressed in https://github.com/ppy/osu/pull/37172, cc @LiquidPL
Would fail in multiple tests. I'm not going to spend time figuring out
exactly why, I'm just going to guess that not all tests bother to set up
the relevant playlist items for the cards or whatever.
Some of the failing tests are flaky but not because the `item` here
isn't sometimes null in those cases. It's always null, but the callbacks
are probably scheduled or whatever and therefore have a chance to never
run. Also some of the failures appear to cascade / spill from other
tests as well.
Exposed by CI failures
([example](https://github.com/ppy/osu/actions/runs/23888446400#user-content-r0s0)).
The race occurs when a consumer calls `GetBindableDifficulty()` for the
first time and then a cache invalidation is triggered. The sequence of
events triggering the failure is as follows:
1. Consumer calls `GetBindableDifficulty()` to get a difficulty bindable
for a given beatmap tracking the game-global ruleset / mods. This
triggers difficulty calculation A.
2. In the meantime, another process requests a cache invalidation for
the same beatmap as the one supplied by the consumer in step (1). This
incurs a cache purge and triggers difficulty calculation B, but never
cancels difficulty calculation A.
3. Difficulty calculation B concludes and writes the correct, latest
difficulty value to the bindable.
4. Difficulty calculation A concludes and writes an incorrect, stale
difficulty value to the bindable.
See below for patch that reproduces this behaviour on my machine 100%
reliably:
```diff
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
index https://github.com/ppy/osu/commit/d6b40639161e26af223f03761b3826b0cd7f4a87..c9604e0e58 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
@@ -252,17 +252,17 @@ private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rules
GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken, computationDelay)
.ContinueWith(task =>
{
+ StarDifficulty? starDifficulty = task.GetResultSafely();
+
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
- Schedule(() =>
+ Scheduler.AddDelayed(() =>
{
if (cancellationToken.IsCancellationRequested)
return;
- StarDifficulty? starDifficulty = task.GetResultSafely();
-
if (starDifficulty != null)
bindable.Value = starDifficulty.Value;
- });
+ }, starDifficulty?.Stars > 0 ? 400 : 0);
}, cancellationToken);
}
```
The goal of the patch is to reorder the write to the bindable in order
to trigger the scenario described above.
Due to the invasiveness of the patch it is not suitable to add as a
test, and chances are that the schedule delay may need to be tweaked if
anyone else wants to check that patch.
Anyway, the solution here is to use the same pattern of creating a
linked cancellation token even on the first retrieval of a bindable
difficulty, and registering it in the list of cancellation tokens that
already existed to service the ruleset- / mod-tracking flow.
Some extra rearranging in
https://github.com/ppy/osu/commit/9184299239a8b5e82957d46db33f1d26bab238fd
is needed to ensure the linked tokens created to do this don't stay
behind after they are no longer needed for anything.
- Last part of / closes
https://github.com/ppy/osu-server-spectator/issues/406.
- Remaining work on slots will be tracked in
https://github.com/ppy/osu-server-spectator/issues/405.
This PR is a corollary of
https://github.com/ppy/osu-server-spectator/pull/453 and all of the
dispensations referee users in a multiplayer have received therein. The
goal here is to allow access to all relevant room management functions
even if the referee in question isn't host, as well as to disallow
access to all non-relevant functions to do with the actual match
gameplay.
I'm not going to lie, this logic *is* ugly. I would argue that it
already *was* ugly on `master` and my goal was to operate with as light
a touch as possible myself. But you could see this as copping out and
that I should try to refactor some of this. I will try - but only after
someone else's seen the initial approach and deemed it unsuitable.
The logic in `MatchStartControl` is awful - there are so many moving
pieces of state that dictate what can happen when with all the buttons,
and yes, I am making it worse here.
This time there is some test coverage. Not everything is covered, but
the coverage should be on par in all components and pieces of relevant
logic I touched that already had tests covering them. On that note,
please forgive the diffstat size, but the tests *are* most of that size.
---------
Co-authored-by: Dean Herbert <pe@ppy.sh>
## [Adjust CI test reporting to upstream action
changes](https://github.com/ppy/osu/commit/f736337b1acf311bf48fb8c39cae67c3f172608d)
It's been semi-broken since I bumped it a few weeks ago. Paper trail for
this is at https://github.com/dorny/test-reporter/issues/750, but to
TL;DR it: `dorny/test-reporter@>=v2.0.0` migrated from [creating new
check runs via the GitHub
API](https://docs.github.com/en/rest/checks/runs) to [job
summaries](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#adding-a-job-summary)
by default.
The main difference is that compared to check runs, job summaries do not
*appear to* require extra `GITHUB_TOKEN` permissions, but in exchange
are limited to the job that called it. This broke visibility of the test
reports because due to `GITHUB_TOKEN` permissions foibles the test
reporter was running in a separate workflow.
I see migrating as a plus here, since:
- The visibility of results is comparable-to-better (example available
for preview at https://github.com/bdach/osu/actions/runs/23892493152)
- No longer required to have a completely separate workflow for test
result reporting
- No longer required to give `checks: write` permissions to the action
(I'd hope, we'll see, untested on a public repository with PRs involved)
One downside is there'll be no in-code annotations for failing tests
anymore but that's whatever IMO. Half the time they weren't even very
helpful, test results pretty much require maintainer interpretation
anyway.
This needs to be applied to a few other repos but I'm starting here
because this is the one where the traffic is highest and therefore
unbreaking the report is of most value (and also the one where I'll see
if it works with public PRs the fastest).
Side note, I was hoping to remove the artifact upload/download games by
just attaching the summary inside each individual test job in the
matrix, but [it looks like
crap](https://github.com/bdach/osu-framework/actions/runs/23888384309)
because only the first three summaries are loaded by default, so if
there are more, you have to click each remaining one to see its output.
Wow. Awesome.
Also updates the action to `v3.0.0` to resolve node deprecation
warnings.
## [Update inspectcode version to resolve deprecation
warnings](https://github.com/ppy/osu/commit/496cf6890ecb6571e6361731d46f53cceb7cb584)
More node deprecation warning fixes.
Mod that colours HitObjects based on the musical division they are on,
now in osu!catch
https://github.com/user-attachments/assets/2dda493d-dc8b-4ea4-ba47-7d04e2062b42
For now *bananas are not coloured by the mod and keep their yellow
colour*, since I think its better for the sake of readabilty (also it
just looks kinda ugly?). Do leave your thoughts on that.
Droplets are always coloured to `LightGreen`, setting their colour to
closest timeline ticks is wrong and looks incorrect since droplets
aren't generated with them in mind.
---------
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
Co-authored-by: Shavix <54279284+Shavixinio@users.noreply.github.com>
Intended to add toggle but forgot.
This also fixes https://github.com/ppy/osu/issues/37012 via a convoluted
refactor of a lot of stuff. The basic overview is:
- Moved all replay overlay concerns out of `HUDOverlay`. We can display
this above everything confidently (i think).
- Split out `ReplayOverlay` and `ReplaySettingsOverlay` so the base
class can handle the visibility, hotkeys and everything that should be
shared with *all* replay overlay components going forward. `Ctrl+H` is
supposed to hide any of these kinds of details, and I'm sure we'll add
more in the future.
- Reorganised some things in `Player` so the new structure would work.
Mainly the overlays which add a black layer during fade out.
Adds the ability to drag and reorder cards. Card order is preserved
between rounds and is synchronized between both players (each player can
see the other player drag around and reorder their cards).
To make this possible I had to rewrite the card layout algorithm to be
stateless (e0a46fefaf), there wasn't
really a way around that since I needed a way to calculate a layout
position based on a card's index. Should hopefully be a lot easier to
read now though.
Some noteworthy stuff:
- I didn't really know what the best place to store the card order is,
so I put it on `RankedPlayCardWithPlaylistItem` since that one will stay
the same instance per round.
- To prevent the opponent's cards from dragged into the middle of the
playfield, only the x axis of the drag gets synchronized for the
`OpponentHandOfCards` with a fixed y value.
- I adjusted the replay recorder/player parameters a little. With the
drag events happening every frame the replay recorder record new frames
every 25ms and end up dropping half the replay frames per flush
interval, so I increased the sample interval to 50ms so the buffer size
matches the sample rate exactly (50ms -> 20 samples per flush every
1000ms).
I also increased the buffer size in the replay player a bit so slight
fluctuations in latency won't make it start to drop frames.
https://github.com/user-attachments/assets/b810cb85-db02-4edf-a63e-bfc96cf59665https://github.com/user-attachments/assets/4d2f884d-fcce-4948-9659-fbb314634cb8
---------
Co-authored-by: Dean Herbert <pe@ppy.sh>