Skip to content

[pull] master from ppy:master - #4

Open
pull[bot] wants to merge 10000 commits into
ebfork:masterfrom
ppy:master
Open

pull[bot] wants to merge 10000 commits into
ebfork:masterfrom
ppy:master

Conversation

@pull

@pull pull Bot commented Feb 8, 2022 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot]

Can you help keep this open source service alive? 💖 Please sponsor : )

@pull pull Bot added the ⤵️ pull label Feb 8, 2022
bdach and others added 29 commits July 21, 2026 21:19
… until current score is done (#38362)

Noticed through monitoring datadog metrics after a deploy.

The client not waiting for the play to complete before switching over to
a new instance of spectator server would cause the replay to get split
in half; the half prior to the switchover would be discarded by the old
instance, while only the half after the switchover would be recorded and
saved to a replay.

Have half-tested via local environment (on master, shutting down
spectator during a play will cause it to start spewing errors instantly
due to attempting to reconnect to a shutting-down instance; with this
change, that only occurs on the next started play).
RFC. This is coming from #38351.

Of note, the reported issue is only reproducible with non-English
language active.

The reason why "localisation is taking a long time to load" is that *the
localisation is not there yet*. The localisation store attempts to
scrape out the string from `ResourceManager`, which needs to walk all
resource files to find the resource, and it can only return the `null`
if every single lookup fails - and currently, for those particular
strings, it does.

<img width="1056" height="409" alt="Screenshot 2026-07-21 at 10 46 53"
src="https://github.com/user-attachments/assets/25223662-63dc-41bb-b2f8-7a1067d646d7"
/>

This is going to only get worse in time as we are overloaded with
requests to merge localisation changes so I am proposing this naive
caching scheme in earnest.

Of particular note, this still doesn't eliminate the spikes of update
thread on filters in the key binding panel - those are still there due
to copious (over)use of `ReadableKeyCombinationProvider` /
`EnumExtensions.GetValuesInOrder()` underneath. But at least this is a
return to baseline.

<img width="1058" height="451" alt="Screenshot 2026-07-21 at 10 47 14"
src="https://github.com/user-attachments/assets/83bb5ad8-8998-4552-a4d1-3ef44b3287fe"
/>

When it comes to cache eviction, in very crude testing with Russian
language (opening a bunch of screens to preload a bunch of strings) I
got the manager instance to occupy ~170kB which is in the "probably fine
to not worry about it" territory.

<img width="1858" height="230" alt="Screenshot 2026-07-21 at 10 46 18"
src="https://github.com/user-attachments/assets/de572f91-d055-466c-ab7d-55dcae118ac0"
/>
- Client-side counterpart to / depends on
ppy/osu-server-spectator#523
- [x] Requires on server deploy of above
https://github.com/user-attachments/assets/f6d2b245-85f6-48ce-865e-1b4e92fe170a

There has been previous discussion about this feature
[here](#23656).

Tests check that the circles are all hidden/visible depending on the
option, judgments are still complete on all hit types and the map
finishes (does not wait because of the optional second hit for
finishers). Should catch future changes breaking the feature silently.

As an aside, I saw that a recent change for the rate the hit objects fly
away there broke rewind in replays. Tried rewinding with these changes
and it still works.

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
Co-authored-by: Dean Herbert <pe@ppy.sh>
)

RFC

Part of #2084

## [Passing & failing
triggers](d84cbec)

Caveats here are very similar to #28480
in that when these are triggered *will* not match stable because
stable's code surrounding that area is one of the worst parts of stable
I've ever seen (`Player.UpdatePassing()`, `Ruleset.IsPassing`,
`Player.Passing`, none of those three behaving in organoleptically
observable practice as I would expect them based on source
inspection...) and I have well run out of my "getting through stable
code" juice for this week.

This implementation is very simple: HP above half = passing, HP below
half = failing.

## [Hit object hit
triggers](df549d7)

For whatever reason these do not appear to work on stable at all in
catch or mania, but they will work fine with this implementation. On
osu! and taiko this implementation should probably be close enough to
not need getting touched in the future, though I have not tested that
very much.

## Hit sound triggers: [The rest of the proverbial
owl](https://knowyourmeme.com/photos/572078-how-to-draw-an-owl)

These are not implemented here for two reasons:

- The logic to when these fire is significantly more complicated
conceptually than the other two based on source inspection
- I have no idea how I am going to couple hit sound playing with
storyboard triggering. On stable, which treats component layering as a
loose suggestion at best, this is not as much of a problem - but when
you want to actually separate concerns, it's a bigger pickle. The
closest thing lazer has that *may* somehow be usable here is
`DrawableHitObject.PlaySamples()` but I generally just do not want to
get into those weeds too deep at this point in time as this is an L-size
diff stat anyhow.

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
This just feels right. Very weird interaction when hitting Ctrl+N with
the login overlay visible otherwise.
Alternative to #38358.

This is the most I'm willing to compromise. So we can take this in two
directions:

- Add this in and see how many users end up finding it amicable as an
alternative to skin hacks (not going to satisfy everyone as per
referenced PR discussion).
- Close this and leave users to continue to hack via skins.

For what it's worth, using the skin number hack in stable/lazer classic
skins is NOT and never has been "insta-fade", it's [still 60
ms](https://github.com/ppy/osu/blob/a4f66f5988d5079875ee2095b7333d05062596f5/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs#L181).
This change is actually making the fade out faster than that hack. The
only difference is that a bit of animation remains.

I believe this should be amicable to a majority of users, which means we
may be able to move forward with less hacky skins. 🤷

Reviewer's note: A pass on code quality and general non-subjective
checks would be appreciated, along with an approval. I'll make a final
call as to whether to merge or not.

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
Noticed some gigabytes of traffic during a full test run.
…elapsed time (#38369)

Tangentially related to #38359, as the symptom makes a player area
appear to stutter while spectating a multiplayer room.

---

`SpectatorPlayerClock.ProcessFrame` accumulates the elapsed time from
the master clock. In some cases, `ProcessFrame` is called more than once
in the same Update loop, causing it to accumuate the elapsed time twice.
For example:

When calling `GameplayClockContainer.Start` (e.g. resuming a player's
clock after initial sync of players),
- `StartGameplayClock` is scheduled after children update via
`SchedulerAfterChildren`
- In the same Update loop, `FramedBeatmapClock.Update` calls
`SpectatorPlayerClock.ProcessFrame`
- Towards the end of the Update loop, the `StartGameplayClock` schedued
earlier runs, which calls `SpectatorPlayerClock.ProcessFrame` again (2nd
time in this Update loop)

In most cases, this stabilizes after a few start/stop loops as the
elapsed time on the master clock is small, but when the update thread
runs at a low frequency (e.g. 60Hz when tabbed out, common when
streaming tournament matches with the client tabbed out), the master
clock elapsed time overshoots `SpectatorSyncManager.SYNC_TARGET` and
causes a player's clock to Start and Stop indefinitely.

| master | <video
src="https://github.com/user-attachments/assets/93584955-7163-4989-b760-23c5bc77cbf1"
/> |
| --- | --- |
| PR | <video
src="https://github.com/user-attachments/assets/f53d66cb-c733-4d6e-acca-f8795c983f34"
/> |

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
…ing (#38200)

Seems like the most maintainable way to do this.

Using `IBeatmap`'s `HitObjects` and keeping the old method doesn't
really work as we want to improve support for mods where clock rate may
vary per object, and using `StartTime` on the DHO itself is really the
only way we can guarantee this.

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
Hopefully each commit should be self-explanatory.
It's a recurring question we get: why is my game on "lazer" when i chose
"tachyon"?

Until we maybe decide to change how this works (not sure what makes more
sense and don't really want to change anything for now), this should
help alleviate user concerns. In theory.

<img width="657" height="326" alt="osu! 2026-07-27 at 07 39 39"
src="https://github.com/user-attachments/assets/2a953a20-f7bf-4710-acd5-b3b03fe1b902"
/>

---

This also drive-by fixes another informational message not correctly
being shown since the settings v2 design updates (was never bound).
…ardised score conversion algorithm (#38321)

- [x] Pending sheet generation & verification

> [!WARNING]
> The deployment of this fix, if accepted, will consist of
reverification of **all** mania legacy scores.
>
> Of particular note, #38203 is also a
change that requires server-side reprocessing and that reprocessing has
not happened yet as far as I am aware.

Coming from #38102 (but does not
fix it).

Compare:


https://github.com/ppy/osu/blob/d54366dd1192c030b133d85887a19220a4024399/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs#L28-L33
https://github.com/ppy/osu/blob/d54366dd1192c030b133d85887a19220a4024399/osu.Game/Database/StandardisedScoreMigrationTools.cs#L336-L339

Note once more that this *does not fix* the reported user issue. In that
particular instance, the outcome will just be flipped (the lazer score
will be favoured over the stable score). Unfortunately there is no way
to improve the estimate any further to reproduce the exact result. The
reason why is that the combo portion is calculated as


https://github.com/ppy/osu/blob/d54366dd1192c030b133d85887a19220a4024399/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs#L35-L38

The key factor in the above calculation is `result.ComboAfterJudgement`,
which is not replicable without being able to access full judgement
history for a score, which is not available for stable scores unless the
play is somehow fully simulated headless.
…38429)

Closes #38388.

Honestly we may want to do this a different way in the future. If more
buttons are ever added, this will still end up looking wrong. These
screens which already have big elements likely need UI scale applied at
a fractional ratio or not at all.


https://github.com/user-attachments/assets/30acf869-115e-4bf6-b386-04df25c878f2
…ing (#38451)

Closes #38437.

`string.Join()` is not safe to use on arbitrary objects even inside of
`FormattableString.Invariant()` invocations. What actually happens is
that the result of `string.Join()` is computed first before any of the
`FormattableString.Invariant()` machinery kicks in, which means that the
values being joined will be formatted using the ambient culture rather
than the invariant one.

Additionally the hand-rolled parsing in `LegacyBeatmapDecoder` (done
manually because `Parsing` helper methods do not return whether the
parsing failed) was culture-sensitive.
### e7c0487 Update various terms and
locations of some settings

Just some stuff I saw and couldn't ignore. Cases where localisation keys
are not changed are intentional (will not remove translations) because
they are very minor changes.

### b760afe Add a note about using
"change folder location" for existing data

Has come up a few times in help channels. I doubt this will do much to
shift discoverability, but better than nothing?
RFC.

This is what I interpret
#36329 (reply in thread)
to mean lacking further elaboration.

See comparison video:
https://drive.google.com/file/d/1bLnkH8bAWlXwafw59O2uoroQp-53fkLF/view?usp=sharing
(cannot embed directly due to size).

On "normal resolutions", i.e. anything between 4:3 and 16:9, this
matches stable. On "abnormal resolutions" there is no stable to match
and lazer has a bespoke behaviour of scaling the playfield, that I've
attempted to compensate for in
cc35065.
It *kind* of does the job but is not great.


https://github.com/user-attachments/assets/febe0e6c-4ece-4971-b477-95fca12faa54

What any of this means for any existing taiko flashlight scores is not
for me to decide.

The other discrepancies pointed out in the discussion thread will be
addressed separately because they have wider-ranging implications.

<details>
<summary>osu-resources diff for crosshair on flashlight</summary>

```diff
diff --git a/osu.Game.Resources/Shaders/sh_Flashlight.h b/osu.Game.Resources/Shaders/sh_Flashlight.h
index fcfc34d2..493effc9 100644
--- a/osu.Game.Resources/Shaders/sh_Flashlight.h
+++ b/osu.Game.Resources/Shaders/sh_Flashlight.h
@@ -17,6 +17,13 @@ void main(void)
 {
     // todo: workaround for a SPIR-V bug (ppy/osu-framework#5719)
     float one = g_WrapModeS >= 0 ? 1 : 0;
-
-    o_Colour = mix(getColourAt(flashlightPos - v_Position, flashlightSize, v_Colour), vec4(0.0, 0.0, 0.0, v_Colour.a), flashlightDim) * one;
+    vec2 offset = flashlightPos - v_Position;
+    
+    o_Colour = mix(getColourAt(offset, flashlightSize, v_Colour), vec4(0.0, 0.0, 0.0, v_Colour.a), flashlightDim) * one;
+    
+    if (abs(offset.x) < 2.0 || abs(offset.y) < 2)
+        o_Colour = vec4(1);
+    
+    if (abs(length(offset) - length(flashlightSize * flashlightSmoothness)) < 2)
+       o_Colour = vec4(1);
 }
\ No newline at end of file

```

</details>
…#38380)

Resolves #38191

forwarding my thoughts from the issue for visibility:

I thought about this idea and I strongly think it'd improve familiarity
for mappers coming from stable, it mimics stable's UX of focusing on the
timeline when placing objects and not having to pay attention to
placement on the playfield.

I tried mapping with it a bit and I found it significantly more natural
as a long-time stable mapper trying to move to lazer.

one may wonder why not just have your cursor stationary on the hit area?
from my experience mapping multiple times on lazer, I found this to be
one extra thing to focus on having correct which gets annoying in
prolonged sessions. the way this mimics stable's UX makes it all that
natural IMO.

This is a default-off setting with it being under the "show speed
changes" one.

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
https://github.com/user-attachments/assets/333a626c-d5b7-4caf-a3a5-b148a0b33a19

---

RFC. Continuation of
#2084 (comment).

In general I ask this to be treated like a first pass at this feature
rather than a complete pristine implementation.

## Behaviour & known issues

Most testing that I've done is with the test map I made and linked in
#2084 (comment). This is
*close enough* to stable, but differs in several key aspects. Of most
note:

- Some hit sounds will activate a trigger on stable but will not
activate a trigger on lazer. The one case I know of where this happens
is with triggers of the form `HitSoundAllSoft`. The intent of the
trigger is to activate "for any sound, with any normal bank, and `soft`
addition bank". The difference is when a hit object does *not* have an
addition sound. stable still considers that object to have "`soft`
addition bank" despite not having an addition sound, and in lazer that's
not the case, because when the addition sound and its associated
`HitSampleInfo` isn't present, there's nowhere to put its bank, so the
trigger cannot fire.
- Some hit sounds will not activate a trigger on stable but will
activate a trigger on lazer. This occurs for e.g. slider ticks and
repeats. Addressing this would require even further granular control of
when a `DrawableHitObject.PlaySamples()` call activates a trigger and
I'm not sure that's in our interest. The stable logic as to which
actions on which objects activate triggers is very ad-hoc.
- Hitting the taiko drums and the mania keys activates triggers even if
they don't hit any specific object. This matches stable mania but does
not match stable taiko. Unifying this behaviour seems rational and I
think this is the better direction.
- There are other issues I'd rather not even name other than mention in
passing (some maps don't work *quite* right with singular sprites not
fading out correctly, seeking sometimes breaks things...). I would ask
to disregard those and let people report these issues as they are
spotted, and the issues can be fixed later as bandwidth allows.

## Performance

There are two factors to consider for performance here:
- The added overhead from DHOs updating
`GameplayState.LastPlayedSamples`, and
- The actual overhead from determining if the triggers should be played
and then adding the transforms for them.

With the first part, I'm pretty convinced it's not a *large* issue
(measured with storyboard turned off):

<img width="878" height="128" alt="Screenshot 2026-07-24 at 13 11 46"
src="https://github.com/user-attachments/assets/6b9e801a-7aa9-4064-b81b-4a61e11ceadc"
/>

The second is somewhat more dicey, but in large part most of the
overhead is not really negotiable in scope of this PR as it is applying
transforms. I added a slight optimization for it in
f985bc0
and in general it seems good enough in that basically all of the
overhead left is the transforms.

<img width="1114" height="485" alt="Screenshot 2026-07-24 at 14 23 10"
src="https://github.com/user-attachments/assets/58d75c17-5a3d-4207-a372-ad493729c476"
/>

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
- [x] depends on ppy/osu-framework#6778
- closes #38405

port of ppy/osu-web#13079

Wiki is already live with this new styled alert
(ppy/osu-wiki#14941)

I decided to use this component in `OsuMarkdownContainer` because styled
alert is used in both wiki and news (if we will implement news page
fully in the future).

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
How this came to be: I looked at the last page of CI results, went
through all failures, and attempted to fix any single one that was
either directly reproducible or seemed to maybe have an easy fix in 5
minutes or less.

From a representative sample size of N=1 test runs on my fork this
perhaps maybe possibly hopefully helps. (Feel free to wait out CI on
this one to bring that tally to a whopping N=2.)

See commit messages for explanations.
#38464)

Completely random diff for no reason. If unacceptable feel free to just
close without justification.

It's still horrible but the rest of the bad things in profiling are not
easily resolved.

## [Fix O(n²) overhead from individual blueprint additions / removals by
batching](a6913b2)

`BlueprintContainer` enforces sorting, which means that adding hundreds
of items to it one by one essentially incurs an insertion sort which is
quadratic and a major, quickly avoidable, hotspot in profiling.

Resolved by adding items to temporary unsorted list and propagating
insertions / removals once per frame.

| before | after |
| :-: | :-: |
| <img width="1002" height="411" alt="Screenshot 2026-07-29 at 11 33 51"
src="https://github.com/user-attachments/assets/2ef81250-9e74-4782-bdef-1f8431327392"
/> | <img width="1121" height="590" alt="Screenshot 2026-07-29 at 11 34
29"
src="https://github.com/user-attachments/assets/afa0af79-2fc7-4b5d-ac5f-15c74fe4313c"
/> |

## [Fix O(n²) overhead from timeline blueprints re-sorting on every
depth
change](f312006)

Another very avoidable hotspot in profiling.

Selecting and deselecting items causes them to be either brought to
front or sent to back via `ChangeChildDepth()`. To make matters worse,
`ChangeChildDepth()` in the timeline specifically would incur a full
sort of the list every time to cover off a bug scenario that could occur
when a placement is in progress (as it is not ensured that the
placement's location in the list of blueprints is correctly sorted as
the placement's properties change; see
#30377).

In the case of a plain selection this is just pure overhead for nothing,
so change the logic to only perform the sort when a placement is active.

| before | after |
| :-: | :-: |
| <img width="1396" height="546" alt="Screenshot 2026-07-29 at 11 45 04"
src="https://github.com/user-attachments/assets/2e3e81e6-4e9b-4525-962e-7d897372cb1b"
/> | <img width="1373" height="526" alt="Screenshot 2026-07-29 at 11 47
04"
src="https://github.com/user-attachments/assets/6dfaf119-bc3e-4663-82e1-2b52291ce305"
/> |
This migrates `MenuParallax` over to `MenuParallaxScale`, which can go
from 0-4, which lets people dial in the parallax to a value that feels
comfortable (or to crank it all the way up like a nutcase).

This pull request is not AI-generated, AI-assisted, vibe coded, or
whatever people call slop nowadays.

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
Co-authored-by: TheRealStevie <75444413+TheRealStevie@users.noreply.github.com>
…map has null `MD5Hash` (#38480)

Closes #38479, also reported in
https://osu.ppy.sh/community/forums/topics/2230279?n=1.

The logs attached in the forum thread show

2026-07-31 00:05:50 [error]: Unobserved exception occurred via
FireAndForget call: Value cannot be null. (Parameter 'key')
2026-07-31 00:05:50 [error]: System.ArgumentNullException: Value cannot
be null. (Parameter 'key')
2026-07-31 00:05:50 [error]: at
System.Collections.Generic.Dictionary`2.FindValue(TKey key)
2026-07-31 00:05:50 [error]: at
System.Collections.Generic.Dictionary`2.TryGetValue(TKey key, TValue&
value)
2026-07-31 00:05:50 [error]: at
osu.Game.Screens.Select.BeatmapCarouselFilterGrouping.defineGroupsByCollection(List`1
carouselItems, List`1 allCollections)
2026-07-31 00:05:50 [error]: at
osu.Game.Screens.Select.BeatmapCarouselFilterGrouping.getGroups(List`1
items, FilterCriteria criteria)
2026-07-31 00:05:50 [error]: at
osu.Game.Screens.Select.BeatmapCarouselFilterGrouping.<>c__DisplayClass33_0.<Run>b__0()

There are two `TryGetValue()` calls in the mentioned method:


https://github.com/ppy/osu/blob/6b2606c51b4334f36d9d09dc96dc5d6bafb468a1/osu.Game/Screens/Select/BeatmapCarouselFilterGrouping.cs#L425
https://github.com/ppy/osu/blob/6b2606c51b4334f36d9d09dc96dc5d6bafb468a1/osu.Game/Screens/Select/BeatmapCarouselFilterGrouping.cs#L441

Checking the realm database schema as presented by Realm Studio,
`BeatmapCollection.BeatmapMD5Hashes` does not permit nulls, but
`Beatmap.MD5Hash` *does* permit nulls. So in theory only the latter
appears possible.

As to how `Beatmap.MD5Hash` *becomes* null? - no idea. The only thing I
can think of is some external interference. I can't see any actual game
code path that would write a null there. But given how crippling the
result of that null is here (the carousel completely disappears), I
think it's probably better to guard against it?
bdach and others added 30 commits October 28, 2026 20:12
https://github.com/user-attachments/assets/66c3f8b5-7e3d-48e4-8e95-8513ca32b839

---

Part of #24996.

The most interesting part here is the `LegacyTextureLoaderStore`
changes. Stable buttons are skinned using three sprites:
`button-{left,middle,right}`; the side sprites are used unaltered, while
the middle one is stretched to fill space as required.
[`NineSliceSprite`](ppy/osu-framework#6380) can
handle this, except for the fact wherein it wants *one* texture to work
with for the entire drawable and not one texture per region. This is
worked around here by splicing the button textures into a single texture
& lookup. If that offends I can explore alternative changes to framework
that could permit accepting an array of textures there instead.
https://github.com/user-attachments/assets/3019ff0c-300a-4f5e-842a-d6d2a4a774b4

---

Part of #24996

Caveats:

- argon design:
  - Lacks footer content, this will be added in follow-ups.
  - Sound design is rudimentary.
- Appearance and transitions are subject to at-will adjustments. I'm
sure @peppy will have many to apply.

- legacy design:
  - No white flash at the end of the animation sequence.
  - Missing `applause.wav`.
- New mod display is used instead of any skin assets. This can be
discussed further in #37552. For one
thing, I will remark that if mod icons are to become fully skinnable,
then icons for lazer-specific mods will need to be added for the classic
and retro skins so that things do not look stupid. It is also very
unclear, how - if at all - adjusted settings would be highlighted on
those icons.

- in general:
- Demonstrated freeform skinnability capabilities may be constrained in
order to prevent terminal breakage of this screen. More on this later.
- I currently have the intention of providing only one variant of the
extended results screen ("online ranking" in stable parlance) and it
will *not* be the stable one, as the stable one is just kind of bad.
| Before | After | Web |
| --- | --- | --- |
| <img width="652" height="353" alt="Screenshot 2026-11-01 at 2 42
08 PM"
src="https://github.com/user-attachments/assets/baef5f77-5ec7-4037-ad0c-400f58f126f4"
/> | <img width="634" height="364" alt="Screenshot 2026-11-01 at 2 41
18 PM"
src="https://github.com/user-attachments/assets/b526ff1e-73ad-43b1-841f-23ce87b79578"
/> | <img width="420" height="250" alt="Screenshot 2026-11-01 at 2 40
11 PM"
src="https://github.com/user-attachments/assets/8dd56643-bee9-48c2-abc9-80a64aa897fa"
/> |
…9095)

i noticed that in some cases displayed total play time differs from
osu!web, so i decided to do some refactoring and also add localisation
support

[osu!web
reference](https://github.com/ppy/osu-web/blob/70b3b6f34b9371e8f738cc40f67260f283324444/resources/js/profile-page/play-time.tsx#L15-L37)

| case | master | PR | osu!web |
|:-:|-|-|-|
| hours < 2 | <img width="190" height="154" alt="image"
src="https://github.com/user-attachments/assets/ab1bc0c4-f1b4-4d4c-9e47-37976c72f57c"
/> | <img width="195" height="150" alt="image"
src="https://github.com/user-attachments/assets/1a8548ca-b949-478c-b82e-4ace45ef8a96"
/> | <img width="146" height="87" alt="image"
src="https://github.com/user-attachments/assets/6b2a9afc-dd9b-436d-b200-a0c6f1de84b4"
/> |
| hours > 2 | <img width="175" height="142" alt="image"
src="https://github.com/user-attachments/assets/1e0a6b15-8fbd-45d8-8237-7b1b9506456e"
/> | <img width="180" height="153" alt="image"
src="https://github.com/user-attachments/assets/7999e052-373e-46f9-93e9-efbf6aee8f7d"
/> | <img width="130" height="85" alt="image"
src="https://github.com/user-attachments/assets/5e717250-9998-44b8-a399-ba63dc021cd7"
/> |
| some random big time | <img width="199" height="141" alt="image"
src="https://github.com/user-attachments/assets/c2c79154-1ad6-4258-b6bf-441ca58cbfa4"
/> | <img width="191" height="140" alt="image"
src="https://github.com/user-attachments/assets/2c91f8bb-52b3-4c71-bd4c-6daca1ffe2e6"
/> | <img width="120" height="88" alt="image"
src="https://github.com/user-attachments/assets/91eb14c4-5399-48ea-aa0c-ab3034eaa554"
/> |
…39100)

Note: While investigating this, I have found [a
PR](#13657) saying that this specific
test scene can't be refactored in this way until `RoomManager` is
removed, which did seem to happen in
[here](#31866). Also from my testing, and
the fact that everything is going through the lounge and
`MultiplayerClient`, I don't think this should cause any issues. Feel
free to close if I'm wrong, however.

There seems to be a few multiplayer test cases that are using the
`TestMultiplayerComponents` screen instead of the `MultiplayerTestScene`
for providing the dependencies necessary for running multi tests. This
PR aims to refactor them to use the latter, for two reasons:

- the screen stack flow is confusing to read compared to other tests,
given that `TestMultiplayerComponents` creates its own inner stack on
top of the one provided by the test scene, which handles both the online
play screen and any player/spectator screens pushed on top of it. This
seems to have caused at least one [broken
test](https://github.com/ppy/osu/blob/b77b028f9a0256ce23b94ef7eb916648641553e7/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs#L584)
which would always pass regardless of the actual test state, as it
targets the wrong screen stack,
- because of the above, the entire thing breaks when the global footer
is introduced, as `ScreenStackFooter` fails to properly account for the
aforementioned additional screen stack. This came up while I was working
on #39092, where the footer would never
show up on screens spawned during the tests, causing breakage.

Large part of this PR is just
`%s/multiplayerClient/MultiplayerClient/g`, which has been pulled out
into a separate commit for review convenience. The rest is mostly
adjusting the tests to point to the actual `Multiplayer` screen, or
others where appropriate.

There is still one remaining user of `TestMultiplayerComponents`, which
is [this specific test in
`TestSceneScreenNavigation`](https://github.com/ppy/osu/blob/b77b028f9a0256ce23b94ef7eb916648641553e7/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs#L928).
Dealing with that is left for another PR, as I'm not sure how to
properly provide the multiplayer dependencies into `OsuGameTestScene` in
a different way. However, said test appears to be duplicating a
[different test in
`TestSceneMultiplayer`](https://github.com/ppy/osu/blob/b77b028f9a0256ce23b94ef7eb916648641553e7/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs#L264),
so maybe it's fine to just remove it.

- Blocks #39092.
I haven't found a good solution to this yet, even in an isolated
project.

- `--abort-on-container-failure` and `--detach` are mutually exclusive.
This is the main reason I'm applying this revert.
- `--abort-on-container-failure` and `--abort-on-container-exit` are
also mutually exclusive, and fail in a similar way to the above.
- `--abort-on-container-failure` and `--exit-code-from` are also
mutually exclusive, though they are allowed to both be present at the
same time.

I think I'll probably need to look for an external solution to this (or
at least a completely different path), because it doesn't seem like
docker compose can handle it.
This PR adds `Filter` property to `UserTagControl` which allows
overriding the tags which are displayed in it. This is meant to be used
in ranked play (as discussed
[on](https://discord.com/channels/90072389919997952/1476979671886205060/1525627693490765834)
[discord](https://discord.com/channels/90072389919997952/1476979671886205060/1527607201508884633))
to display a prompt allowing players to quickly tag the skillset of the
previously played beatmap.

| Default | `Filter` set |
|--------|--------|
| <img width="737" height="201" alt="image"
src="https://github.com/user-attachments/assets/337af17f-1263-46b3-a285-58f916967463"
/> | <img width="421" height="142" alt="image"
src="https://github.com/user-attachments/assets/3479d63d-f7a8-4e60-8d3b-75ef5b52a3c6"
/> |

Essentially what happens is that all tags matching the filter function
are shown, even if they would normally be hidden because of zero votes.
Additionally, the 'add new tag' button is hidden, as all tags intended
to be available for users are already displayed.

---------

Co-authored-by: Dan Balasescu <smoogipoo@smgi.me>
Currently, if the footer contains any buttons on the right side, and a
blocking overlay is shown, the right buttons will cause any overlay
footer content to be pushed to the left, instead of being anchored to
the right side of the screen.

This PR fixes said issue by pulling the right buttons container out of
the grid which is currently holding the buttons and overlay content.

Alternatively, this could be accomplished through the same button hiding
and reparenting dance which is currently done with left buttons through
`temporarilyHiddenLeftButtons`, however given that:

- I am not aware of the right buttons being intended to open any
  blocking overlays (which would necessitate doing the 'hide every
  button other than the one which triggered the overlay' thing)
- we're not guaranteeing that the left and right buttons won't overlap
  in a case where there's not enough space in the footer for both,
  therefore we're not really gaining anything by having them in the
  grid container,

I decided that this would have added unnecessary complexity for
little/no gain and opted against doing that.

Fixes a bug in #39111.
Requires ppy/osu-resources#440.

This PR adds components rendering both the full and compact variants of
the badge displaying the user's division inside of ranked play.

Also includes a proposed DTO for storing division metadata, which I hope
will later be used when we get web to serve tier/division data to the
client. I prefered doing this over implicitly assuming divisions start
at 600, are spaced 100 ELO apart, etc. since the intention is to have
this metadata served by web anyway.

Regular badges:

<img width="727" height="750" alt="image"
src="https://github.com/user-attachments/assets/f005f912-08ad-4a9a-af1a-565a0d5c6137"
/>

Compact badges:

<img width="590" height="605" alt="image"
src="https://github.com/user-attachments/assets/42e45d12-edf3-44a8-8e92-3f3c89e60301"
/>
#39138)

To be clear: this was already [not supported
server-side](https://github.com/ppy/osu-server-spectator/blob/e63febb9129d1c7377d23ba23af01b57975ae6d5/osu.Server.Spectator/LegacyHelper.cs#L19-L20)
and thus the lack of such guard client-side was raising server-side
errors. The introduction of this guard is not supposed to be interpreted
as a statement against custom ruleset support. At this point in time it
is merely a measure to reduce noise. There are no actual meaningful
behavioural changes incurred by this change.
…#39128)

RFC.

Currently, `ISerialisableDrawable` has


https://github.com/ppy/osu/blob/18b71a28facae9b4ef12dbf2c5b69df98e738175/osu.Game/Skinning/ISerialisableDrawable.cs#L25-L28

The issue with it (other than that nothing actually ever used it) is
that it allows for a very crude degree of control over the relevant
component. You can do either everything or nothing to it. This diff
allows controlling which specific operations apply to which component,
as well as controlling *whether a component can be placed* alongside
others.

That last possibility allows to implement singleton components (e.g.
components which there should be never more than one of), or mutually
exclusive components (if component A is present, then B cannot be). All
of this is relevant for results screen work
(#24996).

Aside from enforcement in skin editor via hiding all relevant controls,
this also adds enforcement at skin deserialization / load time by
ignoring impermissible changes made externally in `.json` files.

If wishing to test this outside of the provided test scenes, one can use

```diff
diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs
index 52dcadd..505bf7586c 100644
--- a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs
+++ b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs
@@ -2,16 +2,20 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Allocation;
+using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Screens;
+using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Localisation;
+using osu.Game.Scoring;
 using osu.Game.Screens;
+using osu.Game.Screens.RankingV2;
 using osu.Game.Screens.Select;
 using osuTK;
 
@@ -31,6 +35,15 @@ public partial class SkinEditorSceneLibrary : CompositeDrawable
         [Resolved]
         private SkinEditorOverlay? skinEditorOverlay { get; set; }
 
+        [Resolved]
+        private ScoreManager? scoreManager { get; set; } = null!;
+
+        [Resolved]
+        private BeatmapManager? beatmapManager { get; set; } = null!;
+
+        [Resolved]
+        private Bindable<WorkingBeatmap> beatmap { get; set; } = null!;
+
         public SkinEditorSceneLibrary()
         {
             Height = HEIGHT;
@@ -88,6 +101,21 @@ private void load(OverlayColourProvider overlayColourProvider)
                                     Origin = Anchor.CentreLeft,
                                     Action = () => skinEditorOverlay?.PresentGameplay(),
                                 },
+                                new SceneButton
+                                {
+                                    Text = "Results",
+                                    Anchor = Anchor.CentreLeft,
+                                    Origin = Anchor.CentreLeft,
+                                    Action = () => performer?.PerformFromScreen(screen =>
+                                    {
+                                        var score = scoreManager?.Query(_ => true);
+                                        if (score == null || beatmapManager == null)
+                                            return;
+
+                                        beatmap.Value = beatmapManager.GetWorkingBeatmap(score.BeatmapInfo);
+                                        screen.Push(new SoloResultsScreenV2(score));
+                                    }),
+                                },
                             }
                         },
                     }

```

and then use the scene library in the skin editor to navigate to the
current draft of new results to test.
as always, changes can be reviewed commit by commit

should be a quick one to get in, i'll leave my comments in a review
below

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
)

<img width="1606" height="954" alt="Screenshot 2026-11-09 at 09 41 54"
src="https://github.com/user-attachments/assets/d00fc33f-e5dc-453a-ba48-a2b10164e57a"
/>

Buttons do nothing as of now, this is visual-only for now.

Intended meaning of buttons:
- "Ranking" expands a view showing a contextually relevant leaderboard
(one of the beatmap global leaderboards in solo, the room leaderboard in
multi, the daily challenge leaderboard in daily challenge, etc.)
- "More statistics" expands a view showing user statistics updates,
performance breakdown, accuracy heatmap, distribution graph etc. (the
equivalent of current screen's when an expanded score panel is clicked)
- "Catalogue" did not feature on any designs and is my invention. The
intent for that screen is to feature controls for
  - (un)favoriting the beatmap
  - rating the beatmap
  - voting for user tags on the beatmap
  - adding the beatmap to relevant collections

These intentions are potentially subject to change through review and
through user feedback as required.

Of note, this component outwardly *appears* to behave like a normal skin
component, and as such it is possible to mix it into legacy skins:


https://github.com/user-attachments/assets/467cea7f-e6e0-499e-b404-591b17787929

This component is mutually exclusive with the legacy "Online Ranking"
button.

---

- Part of #24996
- [x] Depends on resources package containing
ppy/osu-resources#450

From a technical standpoint this is one of the most "inventive" usages
of skinning to date.

- The component itself is basically a fake, in actual usage it displays
nothing of its own, it's basically a stand-in for manipulating the
global footer contents via the API exposed in
#39026
- In the toolbox the footer buttons *are* actually shown so that the
component's button isn't just blank. This is done via checking if
`SkinEditor` available via DI. This trick was previously used in
#33503.

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
Part of #39001

Effect points are now displayed in the objects timeline and have their
own toolbox section. Star icon indicates kiai toggle status. Upcoming
PRs would add tooling for controlling effect points from there.

<img width="982" height="189" alt="image"
src="https://github.com/user-attachments/assets/288c62e6-2988-4741-940b-59663fbb379d"
/>

Remarks:

This implementation works well for osu!/osu!catch given the lines only
indicate kiai toggles, whereas the osu!taiko/osu!mania side is less
ideal given the potential timeline clutter it could cause and the fact
the scroll speed values are being overlapped by objects, which is ironic
because osu!taiko/osu!mania are the modes that really need some sort of
effect points display in the timeline.

Here's how it looks on a common notes speed-up. You can see the scroll
speed values by either hovering over the points or looking at the
toolbox section, but it's very much not ideal.

<img width="1517" height="129" alt="image"
src="https://github.com/user-attachments/assets/48834aa9-14bb-4db3-a020-9c28fcc88e20"
/>

Ideally, effect points would be displayed just like velocity changes in
the timeline, as pills above objects. This works perfectly for osu!taiko
and osu!mania, whereas it doesn't for osu! and osu!catch as we don't
have space for them because of the velocity pill.

Best of both worlds would be using the line display for osu!/osu!catch
and the pill display for osu!taiko/osu!mania, which leads us right back
to potentially wanting ruleset-specific blueprints as PR'd in
#29973. Easiest approach that doesn't
involve said PR would be to conditionally render effect points as
`ruleset has scroll speed ? render pill : render line`.

This is what would this suggestion look like:

<table>
<tr>
 <td>osu!/osu!catch
<td> <img width="848" height="118" alt="image"
src="https://github.com/user-attachments/assets/a7a07116-d573-46d4-a4e0-17437009d72c"
/>
<tr>
 <td> osu!taiko/osu!mania
<td> <img width="1180" height="112" alt="image"
src="https://github.com/user-attachments/assets/53607291-3b8a-45d5-a980-1d848b84355c"
/>
</table>

Personally I'm leaning to the approach above but I'll need clarity on
the direction devs want to take with this.

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
This proposal introduces the following changes to username colours in
chat:

- Users who have supported the game and customised their profile hue get
a username colour based on that selected hue.

This is written in a way such that the custom colour does *not* go away
once supporter expires, but also cannot be changed unless user acquires
supporter again. This matches the user profile behaviour.

This addresses #19757 which has
demonstrated significant support.

- Users without supporter no longer get assigned a random colour based
on their user ID, and instead get either an off-white secondary content
colour (if in an overlay context), or straight white.

This addresses #38705 and
partially addresses #23145, though
I expect this particular choice of method of addressing it may be more
controversial than the previous one.

| before | after |
| :-: | :-: |
| <img width="1284" height="430" alt="image"
src="https://github.com/user-attachments/assets/86f2b96a-0bc5-48d0-b17a-5ae70e707bb0"
/> | <img width="1284" height="425" alt="image"
src="https://github.com/user-attachments/assets/dc21b771-42f3-4bec-89e7-e45e10e11e14"
/> |
| <img width="480" height="610" alt="image"
src="https://github.com/user-attachments/assets/35b12c19-c3c8-443f-b432-4b2243167600"
/> | <img width="479" height="616" alt="image"
src="https://github.com/user-attachments/assets/9dfb1020-8806-4074-922a-34f540668548"
/> |
…38795)

Closes #38727.

In freestyle select it seems counterproductive to apply filters that are
persisted to config such as: the user's star difficulty range, convert
preference, and collection selection, since applying those filters could
yield no results and then make the impression that the user has no
choices available.

The caveat here is that this change as written does not change the
visual state of the relevant controls on the screen in any way.
Listening for opinions as to whether that should be done, as it'll
likely complicate matters significantly.

Co-authored-by: Dean Herbert <pe@ppy.sh>
…39155)

RFC. At CS5 this roughly matches the old sizing. Using that as a
control, the algorithm now scales in line with actual circle size, which
feels a lot better when reaching extremities.

Closes #33794 (comment).

| CS | Before | After |
| :---: | :---: | :---: |
| CS0 | <img width="816" height="704" alt="osu! 2026-11-10 at 10 33
49@2x"
src="https://github.com/user-attachments/assets/8bfde576-77c0-4f1f-b8d3-7780072c854d"
/> | <img width="816" height="704" alt="osu! 2026-11-10 at 10 33 35@2x"
src="https://github.com/user-attachments/assets/98ceb1d7-0fb5-4193-b898-129a6a5a4a38"
/> |
| CS4 | <img width="816" height="704" alt="osu! 2026-11-10 at 10 30
27@2x"
src="https://github.com/user-attachments/assets/2d638674-2723-4fca-a32f-1a468247a76f"
/> | <img width="816" height="704" alt="osu! 2026-11-10 at 10 30 50@2x"
src="https://github.com/user-attachments/assets/3e30f42b-02de-43bf-9950-396d03a70d44"
/> |
| CS8 | <img width="816" height="704" alt="osu! 2026-11-10 at 10 32
20@2x"
src="https://github.com/user-attachments/assets/f3bbbb82-e165-4be7-adc1-b5cfbe628971"
/> | <img width="816" height="704" alt="osu! 2026-11-10 at 10 32 07@2x"
src="https://github.com/user-attachments/assets/86c0a54d-75c4-4f62-9c5f-4e34fb48f4d4"
/> |
| CS10 | <img width="816" height="704" alt="osu! 2026-11-10 at 10 32
58@2x"
src="https://github.com/user-attachments/assets/bd408860-1518-484a-bf2e-d69ae74ad183"
/> | <img width="816" height="704" alt="osu! 2026-11-10 at 10 33 07@2x"
src="https://github.com/user-attachments/assets/cd077dc9-9cf5-4d7a-addc-9c688d0c8542"
/> |
https://github.com/user-attachments/assets/1f5da689-d9c4-45a3-a4e3-61def18fdcb8

---

- Part of #24996

Note that the data flow is just to make visuals work for now. I'm not
super sure it will all hold up in the future, especially when online
data sources are involved, but I'll cross that bridge when I get to
integration.
Currently, `QueueController` will instantly transition `ScreenQueue` to
the 'in room' state, without waiting for the actual room join to
complete. This could cause the matchmaking screen to be pushed without
the room being properly set up client-side, if for some reason the join
took longer than the 2 seconds specified on the `pushScreenDelegate`.

This PR makes `QueueController` wait for join success before changing
the queue state, and also moves setting the delay from
`BeginDelayedSequence` to the delegate, since the former is kinda
redundant here.

If I'm understanding things correctly, this is probably the proper fix
for #38482.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.