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 6, 2026 16:30
See
https://discord.com/channels/188630481301012481/380598781432823815/1522142729407037500

Needs testing and probably test coverage. Just PRing as draft for now
for feedback.

Alternative is undoing the optimisation chain, but it'll be a pain and
adds significant overhead to the static diffcalc path.
abs(0) wasn't too great because it can cross over zero when two overlays
are toggled. to avoid this issue, we check if the target is also zero
before rounding the actual value down.

Closes #38233.
Closes #26990.

| | baseline (stable, unadulterated) | lazer | difference |
| :-: | :-: | :-: | :-: |
| **master** | <img width="528" height="528" alt="stable-untouched"
src="https://github.com/user-attachments/assets/50575b95-6446-43f4-aa6a-529b9595e194"
/> | <img width="528" height="528" alt="lazer-master"
src="https://github.com/user-attachments/assets/81faae45-9e61-44a8-b7a3-cfb3fea88c47"
/> | <img width="528" height="528"
alt="stable-untouched-vs-lazer-master"
src="https://github.com/user-attachments/assets/68fbfec8-cc6a-47b8-a27f-110b03d4d05f"
/> |
| **this PR** | <img width="528" height="528" alt="stable-untouched"
src="https://github.com/user-attachments/assets/50575b95-6446-43f4-aa6a-529b9595e194"
/> | <img width="528" height="528" alt="lazer-pr"
src="https://github.com/user-attachments/assets/df2c5573-964d-418d-951c-01359692a25b"
/> | <img width="528" height="528" alt="stable-untouched-vs-lazer-pr"
src="https://github.com/user-attachments/assets/bcb0ec7c-3eeb-4afd-b86d-23018a4e478f"
/> |

---

To explain this change I will first need to explain stable.

How kiai flashing works
-----------------------

As per stable source[^1] linked in
#26990 (comment), kiai
flashing for a hitobject `h` works in the following way:

- Take `h.CreateKiaiSprites()`.
	- For hit circles this is just `DimCollection[0].Clone()`[^2].
- Add `0.1f` to `Depth`.
- Set additive blending.
- Set colour of kiai sprite to accent colour of hitobject plus `max(25,
300 - R - G - B)` to each component.
- Add a fade-out transform.

There are two points of interest here to determine what stable does:
what is `DimCollection[0]`, and what practical effect the `Depth` change
has. To determine that, it is required to understand how a
`HitCircleOsu` is rendered.

stable hit circle rendering
---------------------------

`HitCircleOsu` consists of the following sprites (listed in order of
appearance in source and addition to `SpriteCollection`):[^3]

| Member name | Texture name | Depth | `DimCollection` |
| :--------------------- | ------------------ |
--------------------------------------------------- | --------------- |
| `SpriteApproachCircle` | `approachcircle` | `drawOrderFwdPrio(2000 +
StartTime - LocalPreEmpt)` | not present |
| `SpriteHitCircle1` | `hitcircle` | `drawOrderBwd(StartTime)` | index
`0` |
| `SpriteHitCircle2` | `hitcircleoverlay` | `drawOrderBwd(2000 +
StartTime - LocalPreEmpt)` | index `1` |
| `SpriteHitCircleText` | skin number font | `drawOrderBwd(2000 +
StartTime - LocalPreEmpt)` | not present |

The above already exposes one discrepancy compared to lazer. On master,
the kiai effect is applied individually both to `hitcircle` and
`hitcircleoverlay` - but stable never actually does the latter.

However, it is still unclear at what level in the depth ordering of
sprites the cloned `hitcircle` that represents the kiai flash resides.
Further analysis to determine this follows below.

Depth ordering analysis
-----------------------

First, let's define the two functions referenced above in the depth
column:[^4]

```csharp
        /// <summary>
        ///   Used by hit values.  Has a range of 0.8-1 and loops every 10000 seconds (over 1 hour).
        /// </summary>
        /// <param name = "number"></param>
        /// <returns></returns>
        internal static
            float drawOrderFwdPrio(float number)
        {
            return 0.8F + (number % 6000000) / 30000000;
        }

        /// <summary>
        ///   Used by hitcircles.  Has a range of 0.8-0.2 and loops every 6000 seconds (1 hour).
        /// </summary>
        /// <param name = "number"></param>
        /// <returns></returns>
        internal static
            float drawOrderBwd(float number)
        {
            return 0.8F - (number % 6000000) / 10000000;
        }
```

To simplify further deliberations, let's assume some variables. These
assumptions are a bit shaky, but should not meaningfully disturb further
deliberations.

- `StartTime = 2000`
- `LocalPreEmpt = 1200` (corresponds to AR=5),
- `OverlayAboveNumber = true` (default).

Given the above assumptions, we have:

- `SpriteApproachCircle` with depth 0.8+(2000+2000-1200)/30000000 =
0.8000933333
- `SpriteHitCircle1` with depth 0.8-(2000/10000000) = 0.7998
- `SpriteHitCircle2` with depth 0.8-((2000-2)/10000000) = 0.7998002
- `SpriteHitCircleText` with depth 0.8-((2000-1)/10000000) = 0.7998001
- The kiai flashing sprite has depth of `SpriteHitCircle1` + 0.1 =
0.8998

Which would make the final ordering:

1. `SpriteHitCircle1` on the bottom
2. `SpriteHitCircleText` above (1)
3. `SpriteHitCircle2` above (2)
4. `SpriteApproachCircle` above (3)
5. The kiai flashing sprite above (4) and topmost.

This exposes the second discrepancy: the kiai flashing sprite, despite
being only a copy of `SpriteHitCircle1` / `hitcircle`, is positioned
*above* `SpriteHitCircle2` / `hitcircleoverlay` and will in fact be
faintly visible on some skins as a subtle halo on `hitcircleoverlay`.

To illustrate this, see the following series of screenshots (recommend
viewing these against a dark background):

| stable, kiai sprite with initial alpha set to zero | stable, kiai
sprite with initial alpha set to 1 | difference (with pink rings
highlighting bounds of the `hitcircleoverlay` ring) |
| :-: | :-: | :-: |
| <img width="522" height="522" alt="stable-no-kiai-effect"
src="https://github.com/user-attachments/assets/4fa1240d-daa9-4061-9b6c-6b9db4da13f6"
/> | <img width="522" height="522" alt="stable-kiai-effect-exaggerated"
src="https://github.com/user-attachments/assets/a7408207-04bc-4b68-93d3-f337caf55640"
/> | <img width="522" height="522" alt="stable-kiai-difference"
src="https://github.com/user-attachments/assets/27ad41bf-68d5-4813-a664-2e40f2dcdbef"
/> |

You should see a faint amount of green creep outside of the inner pink
ring, which proves that on stable the kiai effect sprite is in front of
`hitcircleoverlay`.

The fix
-------

To correct the above, the following is done:

- The `hitcircleoverlay` sprite is no longer wrapped in a
`LegacyKiaiFlashingDrawable` instance, because as discussed above, in
stable it is not used for the kiai effect in any way.
- The flashing sprite from the `LegacyKiaiFlashingDrawable` instance
that is wrapping `hitcircle` is proxied out to be in front of
`hitcircle`, `hitcircleoverlay`, and the combo number. This should match
stable's depth ordering behaviour at least in the simple case.

To disclaim from the get-go, I have not looked into worse cases like
slider repeats, as the ordering is significantly more complicated in
that case
(#14561 (comment)). I am
sort of hoping it will be fine, but I will double check if I am made to
- I kind of want to see the reaction to what is in this diff already
before expending even more effort towards this task.

[^1]: Stable reference:
[`Ruleset.UpdateKiai()`](https://github.com/peppy/osu-stable-reference/blob/0e0aa3b29b10c81ef917de0551421274d22cdc79/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1400-L1459)
[^2]: Stable reference:
[`HitObject.CreateKiaiSprites()`](https://github.com/peppy/osu-stable-reference/blob/0e0aa3b29b10c81ef917de0551421274d22cdc79/osu!/GameplayElements/HitObjects/HitObject.cs#L326-L334)
[^3]: Stable reference: [`HitCircleOsu`
constructor](https://github.com/peppy/osu-stable-reference/blob/0e0aa3b29b10c81ef917de0551421274d22cdc79/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L81-L113)
[^4]: Stable reference:
[`SpriteManager`](https://github.com/peppy/osu-stable-reference/blob/0e0aa3b29b10c81ef917de0551421274d22cdc79/osu!/Graphics/Sprites/SpriteManager.cs#L755-L775)
…table (#38164)

Fixes issue [#37532 ](#37532)
Since slider body should start fading after head disappears and slider
body fades in at same time as head, offset body fade out start by head
fade out duration and decrease long duration by the same amount

First ever open-source contribution, yay (I HAVE SAVED HD)


https://github.com/user-attachments/assets/be44a6d6-4d89-49a0-a3db-f57daac6ac12
This is a very loose RFC that needs more testing, but first and foremost
I would like to confirm that I am at least in the vaguely correct
wheelhouse before proceeding further.

Video demo:
https://drive.google.com/file/d/1BWlrohNgfKWDLDEENDunhsNnl6W4sDmY/view?usp=share_link
(too large to live on GitHub).

## [Add liveness probes to
API](5562ed3)

As described in #35736.

Some ground rules:

- I'm not going to attempt to decide where the probe should be hosted,
this PR is mostly agnostic to that anyway.

- When API is in online state, the probe is periodically queried every
minute (interval up for debate).
  - If the probe URL is not set, nothing happens.
- If the client can't reach the probe, nothing happens (or at least
that's the intended design). This is such that client-local outages or
inabilities to reach the probe don't knock the rest of the game offline
*even though everything can be fine with it*.
- If any other deserialisation error or anything else happens, nothing
happens.
- The only time where anything happens is when the probe is up and
actively returns a result that shows that there is an active outage
going on. In this case, API is instantly knocked to `Failing` state.

- When API is in failing state, the probe is queried more actively
(every 5 seconds as written, but this is also up for discussion).
- If the probe actively returns a result that shows that there continues
to be an active outage going on, the client does not attempt to get back
online.
- In *any other circumstance* (read: the probe being disabled,
unreachable, or returning a result that indicates that there is no
active outage), the client will try to get back online.
- The side benefit to this is that this could provide relief from retry
traffic when we have known outages going on.

The reason this is designed this way is to be fail-safe. In particular,
my concern here is that e.g. the probe being potentially unavailable to
Russia users due to network blocks does not completely delete online
functions for those users.

## [Skip attempting submission when API is in failing
state](3d3caeb)
Speeds up gameplay loading when loading is more or less doomed to fail
anyway.

Will also spam notifications about submission not happening. This could
be probably made smarter if we want to (e.g. notify once until API comes
back online), but I'm not super sure we *want* that, and maybe being
super loud about submission failures is the preferred way to go instead.

---------

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

| before | after |
| :-: | :-: |
| <img width="291" height="71" alt="Screenshot 2026-07-09 at 14 44 15"
src="https://github.com/user-attachments/assets/7625aaa7-9192-44aa-9220-1c0571b80370"
/> | <img width="290" height="54" alt="Screenshot 2026-07-09 at 15 00
04"
src="https://github.com/user-attachments/assets/29a2d014-65c8-4e64-be6a-3783cc41f319"
/> |

---

Closes #38255.

Hardcoding a height is a pretty sucky fix but I'm not sure I see better
alternatives and this works for all languages that we have in the game.

This also includes a visual tests so it doesn't require running the game
in release to visually review this. I'm not entirely sure that doing
that is a good idea, I half-expect there to be hell in tests from
touching locale. So maybe wait until this goes through CI green before
clicking any buttons.
Ride inspection failing for this, no idea why.
| this PR | Web |
| --- | --- |
| <img width="491" height="251" alt="Screenshot 2026-07-01 at 10 13
20 PM"
src="https://github.com/user-attachments/assets/01b8d8d0-53b2-45eb-bcc5-10cd03193402"
/> | <img width="360" height="120" alt="Screenshot 2026-07-01 at 10 13
47 PM"
src="https://github.com/user-attachments/assets/d01b6737-0165-40c4-bff7-6860c35cbebe"
/> |

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
https://github.com/user-attachments/assets/a3f7cd65-271a-4604-adb3-94fdc4bac1dc

- Default presets match stable (0.75x, 1.00x, 1.50x).
- Set a non-standard velocity and press the green "+" button to create a
new preset.
- Middle-click or shift-right-click an existing preset to delete it.
- Presets are stored to the `[Editor]` section of the `.osu`, therefore
they are persistent when saved and will also be accessible to other
people that open the map in the editor.

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
…ssing is ongoing (#38272)

<img width="2298" height="656" alt="2026-07-11 01 17 52@2x"
src="https://github.com/user-attachments/assets/7538bbb3-0d58-4974-ba21-7311bd5e75a1"
/>

<img width="846" height="356" alt="2026-07-11 01 18 33@2x"
src="https://github.com/user-attachments/assets/7092c70b-1899-43ae-97d8-a728308366f5"
/>
Some people are having too much fun with it. Intentionally PR'ing
against `master` so that it can be merged without waiting for the next
pp deploy.
<img width="1285" height="435" alt="image"
src="https://github.com/user-attachments/assets/fbdd7e2e-4146-43d3-9c47-30a6fbb3e1b4"
/>

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
"No Release" mod changes the beatmap's notes before "Invert" mod runs.
"Invert" rebuilds the whole beatmap, replacing all the No Release hold
notes with regular hold notes.

This is a workaround, not a real fix — the two mods could compose
correctly if IApplicableAfterBeatmapConversion mods were applied in a
different order (currently determined by ModType).

Reference issue: #37871

Co-authored-by: 汪钰楚 <wangyuchu@ous-MacBook-Pro.local>
Co-authored-by: Loreos <idontcare@mail.ru>
…d calculations (#38276)

Due to `HarmonicSkill` weirdly storing `ObjectWeightSum`, calling
`DifficultyValue` more than once on a single skill results in any strain
count related operations doing weird things. This is fine outside of
timed calculation, as there's never a reason to call `DifficultyValue`
more than once.

Definitely need to check timed calculation more frequently as part of
the PP development process. As part of this change, I have updated the
tests to assert _all_ attributes are equal between timed and not-timed,
rather than just max combo and star rating. With this change, they are
passing.
Addresses #38301.

Originally we just added the extras for mouse buttons, but no real
reason not to do this. Feels more balanced.

Of note, I've also changed the default ordering because it looks better
in the UI.

<img width="480" height="306" alt="osu! 2026-07-15 at 07 50 53"
src="https://github.com/user-attachments/assets/c2f3238a-8903-4450-b086-af9805884fb9"
/>

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
…es (#38314)

RFC.

The angle to this commit is to possibly close
#26559.

Enabling frame stability in the editor has been tried once before in
#26382, and then reverted in
#26521. The reason for it being reverted
is that it caused a performance degradation, most visible in the mania
editor.

Even in #26382, an inline comment
expressed the hope that "gameplay elements should not require frame
stability". Before I go further, I feel like this comment warrants
addressing.

The problem here is that it is not so much the case that "gameplay
elements require frame stability" for hitsounds to correctly play. The
bigger problem here is that *the editor is reliant on accurate replay
playback to show how gameplay looks like*.

When frame-stable playback is off, the `FrameStabilityContainer` is
allowed to *entirely skip frames* that hit some objects. Due to this,
those objects *never become hit* and are *misses* instead, and *that* is
why they do not play a hitsound.

You can experimentally verify this by commenting out


https://github.com/ppy/osu/blob/43897457fb7b0617125aa05a084e4078633ad4ac/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs#L42

and playing a beatmap such as
https://osu.ppy.sh/beatmapsets/1252588#osu/3203995 in the editor. On my
setup at least, there are several missed objects in the first 20 seconds
of the map.

Given the dependence of editor gameplay preview correctness on replay
playback correctness, there do not appear to be many choices here other
than:

1. Improve replay playback correctness as much as possible by enabling
frame stability as often as possible.
2. Stop depending on replays for editor gameplay preview.

I briefly explored option (2). The idea behind it was to completely
dissociate from the replay subsystem and instead have editor-specific
code that would simulate perfect gameplay *inside*
`DrawableEditorRulesetWrapper` by forcing hits to happen in the correct
time instants on the `DrawableHitObject`s.

After some finagling, I got it to work... only in the osu! ruleset. The
problem with this idea is that *it is not sufficient* to only operate on
the `DrawableHitObject`s. Other rulesets have parts of the playfield
that need to somehow show user input too.

- taiko has the input drum (which notably is the one making sounds).
- catch has the catcher.
- mania has the keys (which notably are the ones making sounds, leaving
aside foibles like maps keysounded via storyboard samples)

Therefore, for that idea to work, *each ruleset* would have to bundle
its appropriate logic for real-time simulation of gameplay, basically
duplicating what replays / autoplay already is in a clunkier way.

Thus I am offering this diff in earnest. The hope is the issues that
were hit in #26382 can be avoided with
additional heuristics.

- Frame stability is only active when the editor clock is actually
running. If it were to be enabled while the editor clock was *not*
running, instant seeks could become non-instant. Moreover, the impact of
it being off in that scenario is reduced as hitsounds do not play when
the editor clock is paused anyway.
- Autoplay regenerations turn off frame stability for a frame. This is
because substituting the autoplay replay under the drawable ruleset
while the editor clock is running would result in the drawable ruleset
replaying the entirety of the new replay from the start until the editor
clock's current time, which results in long frame times and many
hitsounds playing at once.
- Large seeks turn off frame stability as they would also provoke a
clock catch-up, which will result in long frame times and many hitsounds
playing at once. (This is unchanged from the original attempt.)

It is very possible that in the future these heuristics will need to be
expanded further to cover off other scenarios. For instance, I predict
that frame stability may be disabled in the future in the case of live
mapping, though the heuristics above (particularly the autoplay
regeneration case) may be enough to handle that use case.
…#38316)

This is mostly just to reduce visual clutter. The buttons look much less
present when in a disabled state.


https://github.com/user-attachments/assets/5d6275f2-77d8-4399-9602-7635d6b30008

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
I don't think this will be the last pass on these (I want to make stuff
more compact) but this was just to get things aligned for horizontal use
of multiple form components.

| Before | After |
| :---: | :---: |
| <img width="1342" height="1400" alt="2026-07-16 02 27 03@2x"
src="https://github.com/user-attachments/assets/7ccd8064-6d41-4aeb-8fc5-1ea480e484f2"
/> | <img width="1342" height="1400" alt="2026-07-16 02 26 20@2x"
src="https://github.com/user-attachments/assets/dc999b07-0cfe-4bc8-b44f-a926a39d824e"
/> |

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
James Wilson and others added 30 commits October 8, 2026 11:39
I'm not aware of any good reason not to allow this. Seems pretty
unintuitive that connecting via `localhost` works but not `127.0.0.1`.

A single prefix still works here since `localhost` resolves to
`127.0.0.1` anyway.
See ppy/osu-beatmap-difficulty-lookup-cache#22
for rationale.

Regardless of the linked issue, I think this makes more sense anyway.
Keeping `Mods` around just for this case, which is much better solved by
nullables, seems silly.

Intentionally pointed at `master` since this is purely refactor and a
package bump would be required to close the linked issue.
Closes #38036.

Drive-by-fix which should make sense without visual check. And yes, I'm
aware this affects gameplay visuals. None of these visuals were made to
handle 10,000 bpm. Intentions is to roll out this change, limiting upper
animation BPM to 1,200 and seeing if anyone complaints (in the other
direction). I highly doubt that will be the case.

Why was 50 ms / 1,200 bpm chosen? It's the lowest value which still
allows the osu! logo to animate at all.

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
…area / leaderboard (#38105)

Resolves #37977

Currently, long clicking on the empty background space in the beatmap
details area does not trigger the background preview like it does on the
song select carousel.

This MR reverts the blanket changes in #33748, and replaces them with
logic to filter wanted / unwanted OnMouseDown events in
BeatmapDetailsArea.

Any feedback here is appreciated!

Before:


https://github.com/user-attachments/assets/776e4de7-66e8-4488-831b-81fab456d5b0

After:


https://github.com/user-attachments/assets/64b07d84-36ae-4a90-a4a2-6e794f748c4a

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
)

- Builds off #28733
- Addresses #37901,
#38410,
#18654,
#25068,
#34798
- [x] Depends on ppy/osu-framework#6834

Caveats:

- Intentionally called "hitsound volume", not "gameplay volume" because
the intention is to specifically target hitsounds. Gameplay effects
should still be adjusted by the effect volume, as far as I'm concerned.
(alternative take: call this gameplay and include everything non-UI,
then rename "effect" to "UI sounds" or something)
- Affects everything in the playfield and overlay layers. This seems
like a pretty good separation point, which includes things like the
metronome from muted mod. It's about what players will expect from this.
- Does not affect storyboards. I'm sure we're going to get people
complaining about that meaning that "storyboarded hitsounds" are not
affected, but I'm not sure whether this is a correct take or not.
- The following now reads bad. It looks as if the "beatmap hitsounds"
would be a toggle turning them off, not enabling/disabling beatmap
skinned hitsounds. Open to suggestion:
<img width="726" height="456" alt="CleanShot 2026-10-06 at 21 48 33@2x"
src="https://github.com/user-attachments/assets/23cedcdf-fe2f-49c9-b3d6-b6204f1d9af0"
/>

There may be exceptions that come up which we need to handle separately.
I may do a 180 based on how this is received and adjust more than one of
the caveats above. Tachyon feedback will be important here.

I also have started on a redesign of the volume overlay to better house
this. Consider the one in this PR a temporary change.

---

This brings a long-sought feature online. I'm not 100% sure on what this
should or shouldn't cover, so any feedback is appreciated on how this
works for you!

<img width="3428" height="2154" alt="CleanShot 2026-10-06 at 21 50
49@2x"
src="https://github.com/user-attachments/assets/72b714f3-f244-4987-b1bf-ca8e41d23ec9"
/>
…en mod select is open (#38221)

- Closes #38146

When pressing Alt-F4, the mod select overlay never closes and does this
in order:
```c#
SetButton();
clearActiveOverlayContainer(); // inside SetButton()
```

Pressing home or exiting via Esc runs `clearActiveOverlayContainer()`
first, and it doesn't trigger the bug because the
`hiddenButtonsContainer` container gets cleared before the
`temporarilyHiddenButtons` list does. Unsure why `SetButtons()` needs to
clear `temporarilyHiddenButtons`. There's no regression when I tested.

`Hide()`ing the overlay on the screen's `OnExiting()` is also a fix.
`MultiplayerMatchSongSelect` already has `freeModSelect.Hide()`.

I've applied both above as one makes sure the bug doesn't happen anymore
when forgetting to `Hide()` the overlay on the screen's `OnExiting()`
and the other fixes the no pop out animation.

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
Closes #38697.

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
…#39026)

RFC.

This is connected to [ongoing work on the new results
screen](#24996). To justify it, allow
me a wall of text.

The concept of the game-global screen footer that lingered about in past
designs for a while and was kicked into higher gear with song select V2
is a pretty neat concept, but it is also a large pain point with respect
to skinnability.

To function, the global footer must inevitably:

- Be on top of everything (draw order wise).
- Not be part of individual screens (transitions, etc.).
- Must provide a limited API for screens to display content on it.
- Must enforce a uniform look across screens to not look wrong.

All of those are pretty large issues when it comes to skinning. If one
were, for instance, to reimplement the stable results screen inside
lazer (more on this... *later*), the new footer would still make the
results screen look *wrong*, because overlaps aside, the aesthetics
would just not match.

What does this have to do with this refactor? Previously, a screen could
only manipulate the global footer's content on entry - either it
specified a global footer with some buttons, or it did not, and instead
a single back button was optionally provided.

This refactor makes it possible to manipulate the global footer at *any*
point of the screen's lifetime, by introducing
`IOsuScreen.FooterContent` as a plain bindable.

This sort of exposition, in turn, makes it possible to perform the
following parlor trick: Assume a skin component with no actual visuals
of its own, that silently occupies the bottom area of the screen which
coincidentally matches the screen estate taken by the global footer, is
not movable, and is a singleton (only one instance of it can be present
in the skin).

Now that `IOsuScreen.FooterContent` exists, this component could do the
following:

- In its BDL, the component resolves the parenting screen and mutates
its `FooterContent` such that the global footer gets displayed, with
buttons specific to a given screen.
- In its `Dispose()`, the component changes the `FooterContent` to
nothing.

The end result of this parlor trick is that you get a faux-"skinnable
component", whose presence is equivalent with the presence of the global
footer. Thus, it is a *facility to force or suppress it* on specific
screens according to users' whims that already fits into the existing
skinning system (well, save for the "immovable / singleton" parts, but
again - more on this later). Heck, you could even feasibly outfit it
with settings to allow limited control of said global footer content.

The secondary argument for this change would be code quality. The
initial iteration of the global footer resulted in a pretty contrived
API; `IOsuScreen` had all of `BackButtonVisibility`, `ShowFooter`, and
`CreateFooterButtons()`, and `OsuScreen` additionally had
`InitialBackButtonVisibility` that by default *fell back* to another
virtual (`AllowUserExit`).

This refactor replaces all of those members with what is essentially a
handrolled discriminated union. (Actual discriminated unions
***FINALLY!!!*** exist, but they only exist in C# 15 / .NET 11, so it's
going to be a while until I can be their greatest shill.)

Note that there is another reason why the existence of this
discriminated union is particularly convenient. The results screen
designs specified retry / save replay buttons on the far end of the
footer, which is not currently a supported feature. The idea is that
`ScreenFooterContent` could also feature a `RightButtons` member and
this member would allow for implementing that part of the design.
Without it, something like `OsuScreen.CreateRightFooterButtons()` or
similar would need to be called into existence instead.
https://github.com/user-attachments/assets/9b1cf197-608d-4914-a8f9-19c2dc325760

Before any further comments, please note that all of the incoming argon
results screen components are *first iterations* and not necessarily
visually finalised, and this component is the most 'first iteration' out
of all of them.

---

- Part of #24996

The reasons why this is a "first iteration":

- stable rank letter sprites are used because (a) I have nothing better
that's approved for use (cc @peppy) and (b) they're being used on the
ranked play results screen too.

The one problem with this is that there is no dedicated sprite for F
rank because stable didn't have one at all. Up in the air as to what
happens with that.

- Audio design is transplanted from the old results screen without too
much thinking (cc @nekodex). Maybe it is no longer good or applicable.
At this point the point is to have audio at all.
I guess we can see how this goes, even though it's experimental.

Some things to note:
- I have not tested on-device performance of tihs.
- There _have_ been GC-related performance issues (which is exactly
where I'd be concerned), on .NET 11 where this is no longer
experimental. These appear to be on-going developments. See:
dotnet/android#12236
- We have to drop support for `-arm` and `-x86` platforms, though `-arm`
becomes re-available in .NET 11. Probably not an issue, but something to
note.
- This appears to be limited to Android for now - I have not been able
to successfully build iOS with it. Likely available in .NET 11, but
haven't tested.

---------

Co-authored-by: Shiumano <takkunmail2005@gmail.com>
…al footer (#39035)

https://github.com/user-attachments/assets/f5565bf5-348a-44a9-ae5d-cf7a25724821

---

To be used in the [upcoming results screen
redesign](#24996).


667d776
is of particular note. To restate that commit's message with video
demonstration:

> This is something I noticed when implementing the support for buttons
on the right end of the global footer.
> 
> In general, the footer buttons have two pairs of transitions attached:
one wherein the buttons move from/to below the screen, and one wherein
they move to the side. The transitions below were supposed to play when
transitioning from no buttons to some buttons, as well as vice versa
(from some buttons to no buttoins), while the transitions to the side
were supposed to play when transitioning from some buttons to *other*
buttons.
> 
> The issue here is that the side transition was silently broken by
e74f687.
The call to reset buttons to nothing on screen entry caused the
transition to change from playing out horizontally, to playing out
vertically.
>
> Of particular note, this only applies to *entering* a screen. It seems
that *exiting* to an existing screen *still* plays out the horizontal
transition:
>
>
https://github.com/user-attachments/assets/a7bc737e-38bb-4225-8d84-78b7496448e8
>
> This commit fixes this inconsistency as best as it can. The result is
awkward, I will not argue otherwise, but I'm not sure I can see anything
better to offer. Besides maybe completely axing either pair of
transitions.
>
>
https://github.com/user-attachments/assets/75202eb6-390d-4f38-b1e4-6e4bfa0ff68d
#38913)

Merge error is from #30838, where the
entire `TestScenePlaylistsRoomSubScreen` file gets deleted for some
reason. Regressed in that PR too.

Before:


https://github.com/user-attachments/assets/7621195f-8993-4b66-9a9b-32546b01864a

After:


https://github.com/user-attachments/assets/c6bfbfad-c0ca-4c91-a652-06ca2a767407

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
- [x] Depends on framework package containing
ppy/osu-framework#6838

Like ["beatmap attribute
text"](https://github.com/ppy/osu/blob/576dbb2a19b43f556c6ef82b14e90ebad15fea3d/osu.Game/Skinning/Components/BeatmapAttributeText.cs#L29),
but for scores.

This'll come in handy to implement the text in the top left corner of
the legacy results screen design. And in general for people to make
everything custom for themselves (to the degree that the text components
allow).

Supported placeholders:

- `Username`
- `TotalScore`
- `MaxCombo`
- `Accuracy`
- `Date`
- `PP` (where populated, which it won't be on local scores, but I'll
worry about that later - the grid component also has this issue and I
intend to not have both of them calculate pp locally twice *somehow*)
- `Rank`
- `Mods`
- Statistics via `{HitResult}Label`, `{HitResult}`, and `{HitResult}Max`

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
I have no real inkling why (or inclination to find out the specific
details why), but despite `ScoreAttributeText` inheriting
`FontAdjustableSkinComponent`, the latter of which implements
`ISerialisableDrawable`, the `IsEditable` override was not actually
functioning. For it to function the interface has to be restated on
`ScoreAttributeText`.

This is best observed by turning the `IsEditable` property to an
explicit interface implementation; without restating the
`ISerialisableDrawable` interface, turning the property explicit results
in a compiler error.
…web (#39063)

Recent changes in web:
https://github.com/ppy/osu-web/commits/master/app/Libraries/LocaleMeta.php

Tested new language working by looking language section (stating the
obvious because `es` and `es_419` are similar, and if I made a typo like
`es_420` it fallbacks to `es`):

<img width="394" height="192" alt="osu!_vORZt1wkiF"
src="https://github.com/user-attachments/assets/3e42b527-1183-407c-8d1a-44855db6af09"
/>
Closes #39051.

This one is a bit difficult to explain but I shall attempt using visual
aids.

The following discussion will be constrained to this helper method:


https://github.com/ppy/osu/blob/44819244af116e8ae08fe2cbd29c6e489b5c9d4e/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs#L297-L317

We have the following givens:
- An axis-aligned rectangle defined by two points: $\texttt{lowerBounds}
= (x_\min, y_\min)$ and $\texttt{upperBounds} = (x_\max, y_\max)$
- A point $p = (x_p, y_p)$.

The method is trying to determine the range of scalar values of
$\textbf{a} \in \mathbb{R}$ such that $\textbf{a} \cdot p$ is contained
in the axis-aligned rectangle. The following drawing attempts to explain
how this is done in the happy path, exploiting the [intercept
theorem](https://en.wikipedia.org/wiki/Intercept_theorem):

![Pasted 2026-10-20 at 10.34.04
AM.tiff](https://github.com/user-attachments/files/33708021/Pasted.2026-10-20.at.10.34.04.AM.tiff)

The four intersection points of the ray starting at the coordinate
origin and passing through p with the axis-aligned rectangle are
computed, and thus also the scalar factors to bring $p$ to each edge of
the rectangle. Taking the maximum of the factors applied to $p_1$ and
$p_2$ yields the final lower bound, and taking the minimum of the
factors applied to $p_3$ and $p_4$ yields the final upper bound.

However, there is a distinct possibility that the following situation
occurs:

![Pasted 2026-10-20 at 10.34.36
AM.tiff](https://github.com/user-attachments/files/33708246/Pasted.2026-10-20.at.10.34.36.AM.tiff)

In this situation, the ray never crosses the rectangle. The
"intersection" points can still be computed the same, but they will not
be actual intersection points anymore. Applying the same operation as in
the first case will return a *result*, only an improper one - the final
lower bound will be *bigger* than the final upper bound.

Note that the point $p$ being outside of the rectangle is a *necessary*,
but not *sufficient* requirement for this scenario to occur. Consider
the following:

![Pasted 2026-10-20 at 10.47.12
AM.tiff](https://github.com/user-attachments/files/33708479/Pasted.2026-10-20.at.10.47.12.AM.tiff)

In this scenario, $\vec{p_1}$ does run into the special case, but
$\vec{p_3}$ will not - its extension through the origin of the
coordinate space crosses the rectangle, so it'll work fine. That is why
this is not just a direct bounds check.
…39039)

This PR changes the static 'hit' SFX played when doing damage in ranked
play from a single sample to one of six, chosen dynamically based on
context:

Special situations:
- K.O. / Final hit
- Last stand / One-shot protection triggered

Otherwise, it's based on the amount of damage done:
- 1 damage done - easter egg
- 2 - 99,999 - low damage variation
- 100,000 - 499,999 - medium damage variation
- 500,000+ - high damage variation

The breakpoints are kinda arbitrary, so feedback is welcome if they need
tweaking (and/or more breakpoints desired?)

---
- [x] depends on ppy/osu-resources#446

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
https://github.com/user-attachments/assets/62595a2f-62f0-45f4-9021-902c22f1ad98

Caveats:

- osu!: geki and katu counts not available.
- taiko: `hit100k` count not available.
- custom rulesets: rudimentary support.
- Sprite fallbacks may not work properly yet. Will be tested better
against end user skins when everything is in place.

---

- Part of #24996.
- [x] Depends on resources package with
ppy/osu-resources#447 for retro skin to look as
on above video in test scene.

Code sucks in places. I attempted to comment as required but in general
I plead stable.
forot to commit before merge.
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.