- Closes https://github.com/ppy/osu/issues/37884
- Closes https://github.com/ppy/osu/pull/37890
Due to lack of population of `Storyboard.Beatmap` and
`Storyboard.BeatmapInfo` post-decoding, `LegacyBeatmapExporter` would
completely drop background specifications on exported beatmap packages.
This affects both direct legacy export to file (`.osz`) as well as
beatmap submission.
I will not pretend that the API here is optimal but I do not see very
easy opportunities to curtail misuse. Storyboards can be treated as
either parts of a beatmap or standalone entities, and if a requirement
is added to forcibly provide a beatmap and its info when encoding out a
storyboard, I also foresee a requirement to bypass this later when
design mode is implemented, which would be a return to square one.
There is likely room for cleanup around `Storyboard` to maybe make this
nicer (remove passing of both `Beatmap` and `BeatmapInfo` and just pass
`Beatmap` instead, maybe shuffle some properties from `Beatmap` to
`Storyboard` to remove the requirement of having to bolt the beatmap on
to begin with). I leave voicing opinions on that, and how soon that
should be done, to reviewers. My primary intent at this time is to
hotfix a major issue in a released build.
The external editing feature is not involved in this bug and any
attempts to claim so are misdirections.
Internal offset adjust is based on [survey
results](https://docs.google.com/forms/d/1bWdwN9LPB4dJsqh2NO4z0HBiMiod1k8-tJN5oca-1Iw/edit)
results (mean: 17.95, median: 24.5) with slight skewing based on
cherry-picking results and personal experiences.
For users which have had the setting disabled:
<img width="1380" height="774" alt="osu! 2026-05-21 at 09 19 06"
src="https://github.com/user-attachments/assets/0526517a-fa0b-485b-ac1b-b61b2fccd2af"
/>
For users which are already using it:
<img width="1380" height="774" alt="osu! 2026-05-21 at 09 20 24"
src="https://github.com/user-attachments/assets/1bd77b39-5d75-43e8-8fe1-b324064b25fa"
/>
Note the button is intentionally hidden to avoid any confusion (it's
inverse now, so some users may mistakenly click it). Assume if a user is
already on the new engine, they are happy with it.
Test migration dialog in startup game flow using:
```diff
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 4bd5ab83a3..091af6d428 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -1315,6 +1315,8 @@ protected override void LoadComplete()
/// </remarks>
private void applyConfigMigrations()
{
+ dialogOverlay.Push(new MigrateNewAudioDialog(true));
+
// arrives as 2020.123.0-lazer
string rawVersion = LocalConfig.Get<string>(OsuSetting.Version);
```
---------
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
As we go forward, migrations are going to likely become more complex,
requiring access to more components and also at a point in time where
they are ready.
In the upcoming case, `DialogOverlay` and `Audio` are important. Access
to `Audio` is a killer as migrations were run before the `GameHost` has
a chance to initialise it.
---------
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
This change is a prerequisite for making a migration dialog which runs
on startup to let users know that something hs changed.
A few cases this could happen:
- During start (intro still playing)
- During gameplay
Basically making dialogs get poofed without the user ever seeing them.
Arguably, we should also change the way dialogs are still poofed when
activation mode becomes not-`All` (deferring for later response rather
than dismissing?).
- Closes https://github.com/ppy/osu/issues/37757
Commit-by-commit reading is recommended. Commits will be split to PRs on
request but I consider this to be the minimal viable functional
increment.
## Done
- This adds a first version of a full storyboard encoder
(a66dc406f498e35d4e0c8f2a462e946a9a1aeccc). I expect there to be hiccups
due to weird corners of the `.osb` format; this is only intended to be
somewhat correct as a start to build upon. Storyboarders are asked to
file issues as necessary.
- Due to the fact that storyboard definitions can reside both in the
`.osu` and the `.osb`, b60698a95c4de1bfeb36fbb159fd5a6028920832 adds the
required storage to be able to tell which storyboard element lives
where, so that it can be decoded properly later.
- In c9d3e04a4135886b5b0943c85f3cc6f4fe99c84c, the storyboard decoder is
weaved into the beatmap decoder to handle the `.osu` part of the
storyboard, via the
`LegacyStoryboardEncoder.Encode{General,Events}ToBeatmap()` methods. For
`.osb`s, `LegacyStoryboardEncoder.EncodeStandaloneStoryboard()` is
intended, but for now is not used outside tests.
- Because of the above, dd1c4e43dc51154cd67860f096712f8b4f229661 removes
`Beatmap.UnhandledEventLines` as no longer required.
- 26ac417ed98a8937c42e5f52c4e15ef065a48902 adds tests. They are mostly
handwritten to ensure basic encode-decode roundtripping. Using existing
storyboards is difficult, see "Known issues" section as to why.
- 5cc542366db7caac38eb0729260d884905a2c0d5 fixes a bug in the storyboard
decoder where the trigger group number was not properly negated on
decode (see inline comment reference to relevant stable code).
## Known issues
- Any and all variables in the `[Variables]` section are inlined into
their usages by `LegacyStoryboardDecoder`, and as such
`LegacyStoryboardEncoder` will end up inlining them and discarding the
`[Variables]` section. As far as I can tell stable will also do this.
- `LegacyStoryboardDecoder` splits all `M` (move) commands into
`MX`/`MY` commands. Therefore, `LegacyStoryboardEncoder` will write out
things in the same split way. I did not put in effort to attempt to
reconcile this, for reasons of part laziness, part not wanting to bloat
this already-large diff.
- Ordering of storyboard samples on decode may not match the order on
decode. I'm crossing fingers this doesn't matter.
- Part of https://github.com/ppy/osu/issues/37818
- Continued from
https://github.com/ppy/osu/pull/37355#issuecomment-4449248107
## Overview
This PR introduces an alternative API, `ScoreMultiplierCalculator`, to
be used going forward for calculating mod multipliers.
The reason for introducing this new API is that it has been requested
that:
- For any two given mods, it should be possible to have the combined mod
multipliers of them in combination be *different* than the product of
the individual mods' multipliers in isolation, i.e. $mult( \\{ A, B \\}
) \neq mult( \\{ A \\} ) \cdot mult( \\{ B \\} )$.
- For an individual mod, it should be possible to have the mod
multipliers depend on a quantity that is *not* the presence of another
mod or the direct value of a setting on the mod.
This capability is being demonstrated in this PR via the
`osu.Game.Tests.Rulesets.Scoring.ScoreMultiplierCalculatorTest` test
fixture.
## Parity with `Mod.ScoreMultiplier`
This PR contains a `ScoreMultiplierCalculator` implementation for each
of the built-in four rulesets.
The abstract `osu.Game.Tests.Rulesets.RulesetScoreMultiplierTest` and
its four derived ruleset-specific test fixtures were written to ensure
that the new implementations do not diverge from the current state of
affairs.
`Mod.ScoreMultiplier` is not removed in this diff to keep size low. It
will be removed as a follow-up.
## Performance
This PR contains a benchmark comparing the current implementation via
`Mod.ScoreMultiplier` and the new `ScoreMultiplierCalculator` API.
Results below.
<details>
| Method | Times | Mods | Mean | Error | StdDev | Gen0 | Allocated |
|---------------------- |------ |---------------------
|--------------:|------------:|------------:|--------:|----------:|
| ViaModScoreMultiplier | 1 | mods (...)tings [27] | 121.171 ns | 1.5284
ns | 1.4297 ns | 0.0782 | 656 B |
| ViaCalculator | 1 | mods (...)tings [27] | 248.509 ns | 1.9313 ns |
1.6127 ns | 0.1364 | 1144 B |
| ViaModScoreMultiplier | 1 | multiple mods | 128.357 ns | 0.4282 ns |
0.4006 ns | 0.0782 | 656 B |
| ViaCalculator | 1 | multiple mods | 252.953 ns | 1.2860 ns | 1.2029 ns
| 0.1364 | 1144 B |
| ViaModScoreMultiplier | 1 | no mods | 3.007 ns | 0.0345 ns | 0.0288 ns
| - | - |
| ViaCalculator | 1 | no mods | 14.802 ns | 0.0616 ns | 0.0576 ns |
0.0134 | 112 B |
| ViaModScoreMultiplier | 1 | single mod | 40.271 ns | 0.1238 ns |
0.1098 ns | 0.0258 | 216 B |
| ViaCalculator | 1 | single mod | 113.033 ns | 0.3140 ns | 0.2937 ns |
0.0842 | 704 B |
| ViaModScoreMultiplier | 1 | single mod 2 | 3.653 ns | 0.0384 ns |
0.0359 ns | 0.0038 | 32 B |
| ViaCalculator | 1 | single mod 2 | 78.172 ns | 0.0680 ns | 0.0603 ns |
0.0621 | 520 B |
| ViaModScoreMultiplier | 10 | mods (...)tings [27] | 1,169.609 ns |
4.3058 ns | 4.0276 ns | 0.7839 | 6560 B |
| ViaCalculator | 10 | mods (...)tings [27] | 2,575.264 ns | 21.2705 ns
| 19.8964 ns | 1.3657 | 11440 B |
| ViaModScoreMultiplier | 10 | multiple mods | 1,171.775 ns | 6.2332 ns
| 5.2050 ns | 0.7839 | 6560 B |
| ViaCalculator | 10 | multiple mods | 2,579.593 ns | 22.1010 ns |
20.6733 ns | 1.3657 | 11440 B |
| ViaModScoreMultiplier | 10 | no mods | 35.943 ns | 0.1665 ns | 0.1476
ns | - | - |
| ViaCalculator | 10 | no mods | 154.980 ns | 0.2381 ns | 0.1988 ns |
0.1338 | 1120 B |
| ViaModScoreMultiplier | 10 | single mod | 404.185 ns | 1.3190 ns |
1.2338 ns | 0.2580 | 2160 B |
| ViaCalculator | 10 | single mod | 1,167.279 ns | 6.1641 ns | 5.7659 ns
| 0.8411 | 7040 B |
| ViaModScoreMultiplier | 10 | single mod 2 | 42.128 ns | 0.2878 ns |
0.2692 ns | 0.0382 | 320 B |
| ViaCalculator | 10 | single mod 2 | 775.435 ns | 2.3318 ns | 2.1811 ns
| 0.6208 | 5200 B |
| ViaModScoreMultiplier | 100 | mods (...)tings [27] | 11,623.346 ns |
51.7174 ns | 43.1863 ns | 7.8430 | 65600 B |
| ViaCalculator | 100 | mods (...)tings [27] | 25,252.987 ns | 44.4352
ns | 39.3906 ns | 13.6719 | 114400 B |
| ViaModScoreMultiplier | 100 | multiple mods | 11,928.536 ns | 35.2079
ns | 32.9334 ns | 7.8430 | 65600 B |
| ViaCalculator | 100 | multiple mods | 25,399.378 ns | 152.4597 ns |
127.3108 ns | 13.6719 | 114400 B |
| ViaModScoreMultiplier | 100 | no mods | 328.158 ns | 0.5827 ns |
0.5165 ns | - | - |
| ViaCalculator | 100 | no mods | 1,517.485 ns | 10.2304 ns | 9.5695 ns
| 1.3390 | 11200 B |
| ViaModScoreMultiplier | 100 | single mod | 3,986.251 ns | 24.2523 ns |
21.4991 ns | 2.5787 | 21600 B |
| ViaCalculator | 100 | single mod | 11,479.514 ns | 23.3738 ns |
20.7203 ns | 8.4076 | 70400 B |
| ViaModScoreMultiplier | 100 | single mod 2 | 385.679 ns | 3.5190 ns |
3.2917 ns | 0.3824 | 3200 B |
| ViaCalculator | 100 | single mod 2 | 7,658.646 ns | 21.8274 ns |
19.3494 ns | 6.2103 | 52000 B |
</details>
While the calculator is obviously slower, in my view it is not
egregiously so. The main overheads both time- and memory-wise are
collection allocations for the dictionary and the set which I consider
to be directly caused by the requested additional complexity and as such
I don't really consider them eliminable.
I have tried and applied some micro-optimisations
(e2469ce338,
cb33abec17), albeit with negligible
effect. I have also tried to key the mods by `Acronym` instead of by
`Type` and the difference was basically nil.
<details>
<summary>patch for keying by acronym</summary>
```diff
diff --git a/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs b/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs
index 772f9d178b..7f5907cbda 100644
--- a/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreMultiplierCalculator.cs
@@ -13,26 +13,26 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public class ScoreMultiplierCalculator
{
- private static readonly List<(Type[] mods, Func<Mod[], double> multiplier)> combination_multipliers = [];
- private static readonly Dictionary<Type, Func<Mod, ScoreMultiplierCalculator, double>> single_multipliers_with_context = [];
- private static readonly Dictionary<Type, Func<Mod, double>> single_multipliers = [];
+ private static readonly List<(string[] modAcronyms, Func<Mod[], double> multiplier)> combination_multipliers = [];
+ private static readonly Dictionary<string, Func<Mod, ScoreMultiplierCalculator, double>> single_multipliers_with_context = [];
+ private static readonly Dictionary<string, Func<Mod, double>> single_multipliers = [];
/// <summary>
/// Defines a flat, setting-independent score multiplier for the given <typeparamref name="TMod"/>.
/// </summary>
public static void Single<TMod>(double hasMultiplier)
- where TMod : Mod
+ where TMod : Mod, new()
{
- single_multipliers[typeof(TMod)] = _ => hasMultiplier;
+ single_multipliers[new TMod().Acronym] = _ => hasMultiplier;
}
/// <summary>
/// Defines a setting-dependent score multiplier for the given <typeparamref name="TMod"/>.
/// </summary>
public static void Single<TMod>(Func<TMod, double> hasMultiplier)
- where TMod : Mod
+ where TMod : Mod, new()
{
- single_multipliers[typeof(TMod)] = mod => hasMultiplier.Invoke((TMod)mod);
+ single_multipliers[new TMod().Acronym] = mod => hasMultiplier.Invoke((TMod)mod);
}
/// <summary>
@@ -40,20 +40,20 @@ public static void Single<TMod>(Func<TMod, double> hasMultiplier)
/// The multiplier calculation is given additional context to calculate the multiplier via the <typeparamref name="TContext"/> type instance.
/// </summary>
public static void Single<TMod, TContext>(Func<TMod, TContext, double> hasMultiplier)
- where TMod : Mod
+ where TMod : Mod, new()
where TContext : ScoreMultiplierCalculator
{
- single_multipliers_with_context[typeof(TMod)] = (mod, context) => hasMultiplier.Invoke((TMod)mod, (TContext)context);
+ single_multipliers_with_context[new TMod().Acronym] = (mod, context) => hasMultiplier.Invoke((TMod)mod, (TContext)context);
}
/// <summary>
/// Defines a score multiplier specific to when both <typeparamref name="T1"/> and <typeparamref name="T2"/> mods are present.
/// </summary>
public static void Combination<T1, T2>(Func<T1, T2, double> hasMultiplier)
- where T1 : Mod
- where T2 : Mod
+ where T1 : Mod, new()
+ where T2 : Mod, new()
{
- combination_multipliers.Add(([typeof(T1), typeof(T2)], mods => hasMultiplier((T1)mods[0], (T2)mods[1])));
+ combination_multipliers.Add(([new T1().Acronym, new T2().Acronym], mods => hasMultiplier((T1)mods[0], (T2)mods[1])));
}
/// <summary>
@@ -61,7 +61,7 @@ public static void Combination<T1, T2>(Func<T1, T2, double> hasMultiplier)
/// </summary>
public double CalculateFor(IEnumerable<Mod> mods)
{
- var allModsByType = mods.ToDictionary(m => m.GetType());
+ var allModsByType = mods.ToDictionary(m => m.Acronym);
if (allModsByType.Count == 0)
return 1;
@@ -83,7 +83,7 @@ public double CalculateFor(IEnumerable<Mod> mods)
}
}
- foreach (var modType in remainingModTypes)
+ foreach (string modType in remainingModTypes)
{
if (single_multipliers.TryGetValue(modType, out var multiplier))
result *= multiplier(allModsByType[modType]);
```
</details>
One particular parallel thread that may warrant follow-up is that
`Mod.UsesDefaultConfiguration` is disproportionately expensive due to
calling into regexes via Humanizer internals.
<img width="1517" height="517" alt="Screenshot_2026-05-19_at_10 58 30"
src="https://github.com/user-attachments/assets/68309a8c-74e7-4f96-8ef9-62868eeca337"
/>
https://github.com/user-attachments/assets/2a511e0d-51f8-4abf-a3ab-de0992618b6b
This implements a rather opinionated UX that's designed to be a middle
ground between the previous lazer behaviour of unconditionally
inheriting the last slider's velocity and just a textbox that you'd need
to manually fiddle with every time.
As to what that means, precisely:
- By default, the control follows the last slider's velocity (updates on
seeks as well as changes to existing objects).
- When the slider control in the toolbox is manually adjusted, the
control decouples from the last slider's velocity and instead uses the
last manually-specified value.
- There is a button that allows the user to couple back to the last
slider's velocity if they consider to have made a mistake in adjusting
it.
- Upon successful placement of a slider, the control reverts to
following the last slider's velocity.
Of note, this control *only interacts with and affects the next placed
slider*. It is in no way coupled to any selected objects. This may be
confusing to users but was an intentional choice to limit complexity
(what if there are multiple selected objects with multiple velocities?)
For adjusting existing objects you can use the green pieces on the
timeline, which notably do support changing multiple selected objects at
once.
---
- Closes https://github.com/ppy/osu/issues/36844
- Supersedes / closes https://github.com/ppy/osu/pull/33707
We already [did this for quick
play](https://github.com/ppy/osu/pull/36025) but it was never carried
across. Some of the new UI *could* work with UI scale with more
consideration, but for now, let's disable it globally to fix cases like
the results screen which people completely unusable at higher scales.
---
@ppy/team-client would hope to get this into today's build, if priority
review could be given to it.
Closes https://github.com/ppy/osu/issues/37407.
- Note that this adds a UI scale slider to all `ScreenTestScenes`. In
some testing, this seems to work just fine.
- May be worth reading 6c8dd589f7384d46377620340d300e53aec7eed5 commit
message for one future concern i have.
- Added a small breakdown animation to the results screen.
- Added individual multiplier text to user corner pieces.
- Removed global multiplier text from the stage overlay, since we're
going with individual multipliers.
https://github.com/user-attachments/assets/47cec478-6ad5-49fa-9f69-b6df079ce41c
(This is dev design and I'm focusing on functionality rather than
presentation for now.)
The implementation might be over-engineered a bit, but I'm not sure on
the final structure of things and I want to give a bit of elasticity to
the system, so I've frankensteined a new "damage sources" list inside
`RankedPlayDamageInfo` that the results screen uses to display the
breakdown.
If the server doesn't provide a breakdown (e.g. by client and server
being slightly out-of-date), the results screen will behave as it does
on current `master`. In other words this is forwards/backwards
compatible.
---------
Co-authored-by: Dean Herbert <pe@ppy.sh>
This used to be the case, but recently changed with the introduction of
[pausing when inactive](https://github.com/ppy/osu/pull/37100). The
change was intended to work for local gameplay modes, but it makes less
sense for spectator/replay where you may want to be watching in the
background while doing something else.
Raised via email.
Closes https://github.com/ppy/osu/issues/37715.
The user's database contains several scores in which `ScoreInfo.Ruleset`
is null. How this happened, I'm not sure, it's probably custom rulesets.
The proper way to handle this would be to mark `ScoreInfo.Ruleset` as
nullable and deal with the hundred files of fallout, and also the fact
that `ScoreInfo` is an overloaded mess of a model that is sometimes a
database model and sometimes a post-converted online structure with
things backfilled to fit and I'm just not wanting to waste a week here,
so I'm choosing to look away.
Sidebar: You can't just put a null-propagating operator in the previous
conditional too because analysers will scream that `Ruleset` can't
*possibly* be null! So this uses `RulesetInfo.Equals(RulesetInfo?)`
because that can sorta-kinda handle nulls.
Fixes friend list not showing global rank of the users.
I think #37709 can be closed without any further client-side changes
after `osu-web` is made to return global rank on user searches.
The delayed collection evaluation (making it async) is the main fix, but
the debounce seems like good to have from a sanity angle. Alternative
would be `Scheduler.AddOnce` to avoid multiple input events per frame
being handled.
Closes https://github.com/ppy/osu/issues/37615.
the x axis division was set to a fixed number of steps rather than
picking a neat step size, which created uneven numbers. this pr changes
this so an appropriate factor is determined, the x-axis min/max is
floor'd/ceil'd to that factor and the divisions are created based on
that factor. the cumulative rating line also extends to the new end of
the graph.
in addition, the bars of the bar chart are now aligned using the left
edge of the bar rather than the center. in the after-image, note how the
left edge of the bar for 1600 rating aligns with the division. (this is
based on the assumption that a rating bucket, say "1500", spans the
interval [1500, 1600]. if the bucket spans [1450, 1550] instead, i will
revert the change)
before
<img width="632" height="217" alt="image"
src="https://github.com/user-attachments/assets/b7053d43-99bb-4e5b-87a4-dcec37d56b50"
/>
after
<img width="608" height="230" alt="image"
src="https://github.com/user-attachments/assets/3f9e4284-f1b1-4bcb-8f70-f75f4e242b19"
/>
could optimize the while loop into a single mathematical expression, but
this is easier to read imo. lmk if you'd prefer the expression instead