I've seen this suggested quite a bit and is a pretty easy implementation
all things considered.
For now, while on the queue screen, you can open up the dashboard
overlay and select another player to duel. This will bring you into an
unranked lobby.
https://github.com/user-attachments/assets/712897a9-9350-4741-899d-59662c722e43
Song preview keeps playing when clicking "Start Watching" on
`SoloSpectatorScreen`. Making `SoloSpectatorScreen` suspends the screen
rather than exiting it, so neither `clearDisplay()` nor `OnExiting()`
fire.
Closes: https://github.com/ppy/osu/issues/36987
- depends on https://github.com/ppy/osu-framework/pull/6737
Adds simple input settings section for pens that allows disabling the
handler and adjusting sensitivity. The section appears in-between Tablet
and Touch, and only on SDL3 (desktop and mobile). The pen sensitivity is
completely independent from mouse sensitivity.
<img width="537" height="149" alt="image"
src="https://github.com/user-attachments/assets/448eebba-84ea-4daf-8428-3bd07739bd6f"
/>
<br>
Keep in mind that the "Confine mouse cursor to window" mouse setting
also affects pens, feel free to suggest UX improvements. Also, toggling
"High precision mouse" might affect pens on certain configurations.
Edit: added image with updated header. Previously, it was "Device: Pen".
---------
Co-authored-by: Dean Herbert <pe@ppy.sh>
Based off of https://github.com/ppy/osu/pull/37478 with some
improvements such as:
* Simpler `progress` handling (single adjustable value instead of 2)
* 2 less drawable paths (replaced with circlular containers)
* Reworked remaining paths to have as little texture sizes as possible
---------
Co-authored-by: Krzysztof Gutkowski <krzysio.gutkowski@gmail.com>
Co-authored-by: Dean Herbert <pe@ppy.sh>
In ranked play, scores are always counted regardless of whether they are
a fail or a pass. Beyond that, there's also no concept of revival in
multiplayer right now, so players are just stuck at 0 HP with a red
overlay.
Not doing what `ModNoFail` does, which is to hide the healthbar
altogether, because some user skins use the healthbar to skin a gameplay
border.
Closes#37443
Previously it would select the first pool that matches the user's
current ruleset.
At first I tried passing the `MatchmakingPool` object to the
notification and have it passed back to the screen, but with that method
only clicking the notification would show the correct pool, if you enter
ranked play normally then it would show the "default" pool instead.
This method needed fewer changes too.
Based on internal feedback.
I was going to apply other changes (like always posting to sentry) but
don't want to go too far down a rabbit hole, so just fixed messaging a
bit.
Resolves#37486
Original error log:
```
2026-04-22 21:47:58 [error]: System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
2026-04-22 21:47:58 [error]: at System.Collections.Generic.List`1.get_Item(Int32 index)
2026-04-22 21:47:58 [error]: at osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand.PlayerHandOfCards.moveCardFocus(Int32 direction)
2026-04-22 21:47:58 [error]: at osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand.PlayerHandOfCards.OnKeyDown(KeyDownEvent e)
```
Currently,
```osu.Game\Screens\OnlinePlay\Matchmaking\RankedPlay\Hand\PlayerHandOfCards.cs::moveCardFocus```
does not account for the hand being empty (cards.Count ==0 ), and will
attempt to move the card focus in the given direction regardless, which
causes the above index out of range error.
Added empty hand check to moveCardFocus, and a matching test case to
TestScenePlayerCardHand.cs
New test case before changes, recreating the error:
<img width="986" height="232" alt="image"
src="https://github.com/user-attachments/assets/daa62081-c776-44bd-b0d2-382b2dac7938"
/>
After:
<img width="638" height="223" alt="image"
src="https://github.com/user-attachments/assets/d6dcd8b8-8caf-42e3-9999-93dfe3fb6452"
/>
Thing to make release happen.
Reverts #37453
Reverts #37463
Alternative to #37473
Not that I disagree with any of these but I'm just looking to return to
what works so we can do a release because we're on a clock here for
other reasons.
Test which should work but doesn't, so I'm not adding:
```diff
diff --git a/osu.Game.Tests/Visual/RankedPlay/TestSceneOpponentPickScreen.cs b/osu.Game.Tests/Visual/RankedPlay/TestSceneOpponentPickScreen.cs
index f747004bbd..eb8e360d1e 100644
--- a/osu.Game.Tests/Visual/RankedPlay/TestSceneOpponentPickScreen.cs
+++ b/osu.Game.Tests/Visual/RankedPlay/TestSceneOpponentPickScreen.cs
@@ -1,12 +1,17 @@
// 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.
+using System.Linq;
+using NUnit.Framework;
using osu.Framework.Extensions;
+using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
+using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
+using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand;
namespace osu.Game.Tests.Visual.RankedPlay
{
@@ -14,6 +19,8 @@ public partial class TestSceneOpponentPickScreen : RankedPlayTestScene
{
private RankedPlayScreen screen = null!;
+ private readonly BeatmapRequestHandler requestHandler = new BeatmapRequestHandler();
+
public override void SetUpSteps()
{
base.SetUpSteps();
@@ -26,8 +33,6 @@ public override void SetUpSteps()
AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
AddUntilStep("screen loaded", () => screen.IsLoaded);
- var requestHandler = new BeatmapRequestHandler();
-
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
AddStep("set pick state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardPlay, state => state.ActiveUserId = 2).WaitSafely());
@@ -44,7 +49,11 @@ public override void SetUpSteps()
}).WaitSafely();
}
});
+ }
+ [Test]
+ public void TestBasic()
+ {
AddWaitStep("wait", 15);
AddStep("play beatmap", () => MultiplayerClient.PlayUserCard(2, hand => hand[0]).WaitSafely());
@@ -54,5 +63,29 @@ public override void SetUpSteps()
BeatmapID = requestHandler.Beatmaps[0].OnlineID
}).WaitSafely());
}
+
+ [Test]
+ public void TestPickPreviewPlayedOnOpponentPick()
+ {
+ RankedPlayCard.SongPreviewContainer? originalPreview = null;
+
+ AddStep("hover first card",
+ () => InputManager.MoveMouseTo(this.ChildrenOfType<PlayerHandOfCards>().Single().Cards
+ .First(c => c.Item.PlaylistItem.Value != null && c.Item.PlaylistItem.Value.BeatmapID != requestHandler.Beatmaps[0].OnlineID)));
+ AddUntilStep("preview playing", () => originalPreview = this.ChildrenOfType<RankedPlayCard.SongPreviewContainer>().FirstOrDefault(p => p.IsRunning), () => Is.Not.Null);
+
+ AddStep("play beatmap", () => MultiplayerClient.PlayUserCard(2, hand => hand[0]).WaitSafely());
+ AddStep("reveal card", () => MultiplayerClient.RankedPlayRevealUserCard(2, hand => hand[0], new MultiplayerPlaylistItem
+ {
+ ID = 0,
+ BeatmapID = requestHandler.Beatmaps[0].OnlineID
+ }).WaitSafely());
+
+ AddUntilStep("wait for original preview stopped", () => originalPreview?.IsRunning, () => Is.False);
+
+ AddUntilStep("preview playing is opponent's pick",
+ () => ((RankedPlayCard)this.ChildrenOfType<RankedPlayCard.SongPreviewContainer>().SingleOrDefault(p => p.IsRunning)?.Parent!).Item.PlaylistItem.Value?.BeatmapID,
+ () => Is.EqualTo(requestHandler.Beatmaps[0].OnlineID));
+ }
}
}
```
---------
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
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.