RFC.
See https://github.com/ppy/osu/pull/37453#issuecomment-4289403735 for
why.
Of note:
- To facilitate mutual exclusivity of playback `PlayerHandOfCards`
maintains a bindable pointing at the currently playing song preview.
- Because of how card drawables are passed between multiple parenting
drawables, some of which are and some of which are not
`PlayerHandOfCards` instances, DI fails horribly at working with this
bindable unless it is manually managed. See relevant overrides in
`PlayerHandOfCards`.
- I renamed one of the overloads of `HandOfCards.RemoveCard()` to
`DetachCard()` because I found the fact that there are two overloads of
one method that do WILDLY DIFFERENT THINGS utterly *asinine*. (One
overload scrapes the `RankedPlayCard` out for you to plop elsewhere. One
*drops it on the floor entirely*.)
This took way too long to write.
Attempt at adressing the points made in
https://github.com/ppy/osu/pull/37419#issuecomment-4279123323
- `CardContainer` is now being sorted immediately instead of only doing
it once per frame. Given that its only ever gonna have a handful of
children there wasn't really a need to optimize that that in the first
place.
- `HandOfCards.Cards` now exposes `cardLookup.Values` as an
`IEnumerable` instead of exposing the card container's children
directly.
- `HandOfCard` now exposes `GetCardsInDisplayOrder` which returns a copy
of all cards in display order. Since it's making a copy I made sure this
isn't called on any hot code paths.
- `HandOfCard.Clear` previously didn't clear the `cardLookup`
dictionary. Didn't cause any issues since we're not re-adding cards to
the hand anywhere but not good regardless.
Switched to looping over all cards and calling `RemoveCard` to make sure
changes to the removal logic can't get overlooked there again.
---------
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
Suboptimal? Sure. But the primary goal is not to crash. Crashing is a
failure of the game programmer.
Better can be done later.
Remedies/fixes https://github.com/ppy/osu/issues/37421.
Due to the push of the relevant screens being delayed it's possible that
the room goes away between the scheduling of the push and the actual
execution of the push.
This maybe closes https://github.com/ppy/osu/issues/37374 but my hopes
are not high.
Includes some extra cleanups I noticed along the way.
- Supersedes / closes https://github.com/ppy/osu/pull/18129. Reasons I
didn't use that PR are hopefully obvious upon comparing diffs but I can
elaborate if they are not.
- Single metric included for demonstration purposes.
- Do not want to talk about further schema design at this time.
- Specify `OSU_WEBSOCKET_SERVER=1` envvar to enable.
- Can test consumption with [this five minute html
job](https://github.com/user-attachments/files/26839923/index.html)
(works even as a standalone file opened in browser, no CORS bs!)
- There's a lot of inline comments, go read them. There are many WTFs
because the .NET frozen websocket API is weird and stanky and reeks of
the year 2007. The inline comments attempt to explain.
Similar implementation to `BundledBeatmapDownloader`. The notification
should not be cancellable, but there's no easy way to do that right now
other than hiding the notification altogether.
The beatmap availability status visible on the corner pieces is probably
a little too small/hides importance, but that will be addressed later to
make up for this.
Intends to close https://github.com/ppy/osu/issues/37408.
I have got to say, the way ranked play apparently re-invents screen
sub-stacks *again* in a slightly different way to everything else before
had me *very* confused as to why things I would expect to get called
aren't getting called.
Exposed to be running by 48434dd683 which
caused test failures.
You'd think that the `IsDeployedBuild` check would catch it but it
doesn't. `IsDeployedBuild` is `AssemblyVersion.Major > 0`, and the
assembly version is taken from the entry assembly. In tests the entry
assembly is either resharper or nunit.
Closes: #37402
Issue was `HandOfCards.CardContainer.Compare` working under the
assumption that the it would never be called with the same child for
both entries, which can happen when doing a `BinarySearch` (called in
`RemoveInternal`).
This lead to `IndexOf` returning a negative value despite the card being
present in the container, and the drawable getting disposed but not
actually removed.
I also included a precautionary `cardContainer.Sort()` call before the
removal as well, seemed to work without that from testing but better not
rely on that.
TLDR: card container child sorting was unstable due to poor assumptions
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.