In some cases `SliderPath.GetPathToProgress` used to compute the whole
path when it can be not needed since it can be already stored inside
`calculatedPath` list.
Also some of these use cases will no longer require additional array
wheen only readonly access is all we need.
Fixes
https://discord.com/channels/188630481301012481/188630652340404224/1493678774540304505
The rating distribution is updated once every 5 minutes, so there are
periods where it may not include the local user's rating. This is simply
a workaround where it's considered if it's bounded by it.
Am a little surprised this is as easy to handle as it appears to be,
even if not the cleanest presentation (it's an edge case).
https://github.com/user-attachments/assets/edfc8d06-4f04-4876-84a5-dfc83a18f160
Of note:
- Supports both native beatmaps and converts
- Supports key mods (changing key mods will trigger song select refilter
when key count grouping is engaged)
- The option to group by keys is only visible when mania ruleset is
active
- If the user selects key count grouping and then switches to another
ruleset, song select will fall back to no grouping, but this change will
not be written back to config. Only the user changing the grouping mode
manually will reflect in config changes. This is done so that key
grouping persists across ruleset changes, and this even survives game
restarts.
---
I've only done some light behaviour testing on this because this feature
needs a lot of subjective shot calls and I don't want to commit too deep
before I get a temperature check on the shot calls I made here.
In particular some performance profiling of
https://github.com/ppy/osu/commit/7de8f70b1dbbdf2e3f13ba10faf25329abf6468d
may be warranted.
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