Closes https://github.com/ppy/osu/issues/37232.
The actual fix is
https://github.com/ppy/osu/commit/e959b20517497a093d3c00a17457c5d36bf57651;
everything else is window dressing / test harness to ensure I don't try
and do a wrong change like https://github.com/ppy/osu/pull/37251 did. I
recommend reviewing commit-by-commit.
See [this desmos](https://www.desmos.com/calculator/a5yjpacvxa) for
visual explanation of change, I think it does a better job at explaining
this than any words I could type here.
Of note:
- In the end this did only affect 14K but that should never be assumed
when floating point is involved.
- Test cases generated here were generated in stable manually.
- Except for 11 / 13 / 15 / 17K which are not officially supported and
which don't work in lazer due to orthogonal reasons (see comment added
in this PR in `ManiaBeatmapConverter`), decoding in lazer was always
fine.
- My worry was that the old encoding method before this PR could
potentially cause stable to move a note from one column to another but
thankfully that is not the case. The old method of encoding columns as X
positions does not cause issues wherein lazer reads them back
differently than stable after encode.
I checked this by checking out `master`, re-encoding all of the test
stair-pattern nK beatmaps added in this PR on `master`, exporting that
as compatibility, re-importing to stable, and cross-checking that the
decoded beatmap is visually the same on lazer and on stable.
This is important to check because if this wasn't the case, we'd
potentially have cases of actual online beatmaps (remember that we have
BSS now) wherein a beatmap plays differently on stable than on lazer due
to notes moving between columns, and would need to screen for this being
the case and potentially apply corrective / reconciliatory action.
Exposed by CI failures
([example](https://github.com/ppy/osu/actions/runs/23888446400#user-content-r0s0)).
The race occurs when a consumer calls `GetBindableDifficulty()` for the
first time and then a cache invalidation is triggered. The sequence of
events triggering the failure is as follows:
1. Consumer calls `GetBindableDifficulty()` to get a difficulty bindable
for a given beatmap tracking the game-global ruleset / mods. This
triggers difficulty calculation A.
2. In the meantime, another process requests a cache invalidation for
the same beatmap as the one supplied by the consumer in step (1). This
incurs a cache purge and triggers difficulty calculation B, but never
cancels difficulty calculation A.
3. Difficulty calculation B concludes and writes the correct, latest
difficulty value to the bindable.
4. Difficulty calculation A concludes and writes an incorrect, stale
difficulty value to the bindable.
See below for patch that reproduces this behaviour on my machine 100%
reliably:
```diff
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
index https://github.com/ppy/osu/commit/d6b40639161e26af223f03761b3826b0cd7f4a87..c9604e0e58 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
@@ -252,17 +252,17 @@ private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rules
GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken, computationDelay)
.ContinueWith(task =>
{
+ StarDifficulty? starDifficulty = task.GetResultSafely();
+
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
- Schedule(() =>
+ Scheduler.AddDelayed(() =>
{
if (cancellationToken.IsCancellationRequested)
return;
- StarDifficulty? starDifficulty = task.GetResultSafely();
-
if (starDifficulty != null)
bindable.Value = starDifficulty.Value;
- });
+ }, starDifficulty?.Stars > 0 ? 400 : 0);
}, cancellationToken);
}
```
The goal of the patch is to reorder the write to the bindable in order
to trigger the scenario described above.
Due to the invasiveness of the patch it is not suitable to add as a
test, and chances are that the schedule delay may need to be tweaked if
anyone else wants to check that patch.
Anyway, the solution here is to use the same pattern of creating a
linked cancellation token even on the first retrieval of a bindable
difficulty, and registering it in the list of cancellation tokens that
already existed to service the ruleset- / mod-tracking flow.
Some extra rearranging in
https://github.com/ppy/osu/commit/9184299239a8b5e82957d46db33f1d26bab238fd
is needed to ensure the linked tokens created to do this don't stay
behind after they are no longer needed for anything.
Based on feedback I've heard more than once ([most
recently](https://github.com/ppy/osu/discussions/36933)), there's too
much overlap compared to stable. And it can sound a bit "jumbled" as a
result, especially when transitioning between two songs with loud and
"complex" audio.
This also fixes noticeable audio glitching that some users may have been
experiencing when switching tracks.
---
Unless there's any very loud objections, I'd ask that review is on the
code not the audible change. This is the kind of change that I tweak to
my personal spec and then push out to hear the general reaction.
### b8d0dfe2ed Better default preview time
based on global fade
This is an oversight. The preview point wasn't being offset to account
for the fade in. This is something we do when generating web-side
previews, which allows for the actual preview point to be audible,
rather than mid-fade.
### 8de323b68c Adjust fade in and fade out
during track transitions to reduce overlap
Yes, I'm aware that the delay is not accounted for in the constant. This
is intentional because it sounds better. I want this to be an internal
adjustment which is not to be considered by any external usages of those
constants. Just sounds better.
### aab01b0fe5 Remove weird/dated schedule
usage
This causes audible artifacts in playback. The track is not set to zero
volume early enough. Original change was made in [this ancient
PR](https://github.com/ppy/osu/pull/9792) (see
[commit](https://github.com/ppy/osu/commit/688e4479506645bdacee7cdc7ae9ee9deaa5f1b4)).
The original failure does not occur. Tests still pass. I'd argue that if
we encounter this again, it's an issue at the call site, not this code.
This is a drive-by fix, as I noticed this glitching while working on the
main issue here. Arguably should be it's own PR 🤷 .
It's been a while.
Notes:
- `SharpCompress` usages changed a bit. Manually adjusted these, mostly
just renames or adjusted parameters.
- nUnit 3 -> 4 migrated using
https://gist.github.com/peppy/07994386d793a117350cb5f24b156585. there's
a mode in this script to update to the newer `Assert.That` syntax but it
requires fixes and couldn't really be bothered.
- DeepEqual nuked as the only usage was on a disabled test. The reason
it's disabled has been merged upstream, but it's failing for other
(realm) reasons which I don't think is worthwhile to investigate for
now.
- This bumps Moq. I think the author is back in a sensible headspace and
the new version has the stupid shit removed, so probably okay? Nice to
be on a level playing field with packages for once in a long time.
- Automapper is silly, but we've discussed this elsewhere.
- `TestRealmKeyBindingStore` failures are a wildcard, but fixed by using
a more standardised testing method. Dunno why, don't care.
---------
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
- closes https://github.com/ppy/osu/issues/36942
- fixes https://osu.ppy.sh/community/forums/topics/2187041?n=1
## [Ensure Simplified Rhythm mod does not produce beatmaps with objects
out of
order](https://github.com/ppy/osu/commit/f4807b3a42dbf46ea0e669b0ba658feaef9baca5)
This is a last ditch safety.
This mod has more apparent issues (see below) but this is a first step
of restoring sanity.
Aside than the other fix described below I have not attempted to figure
out further why this is happening because the conversion logic is in
over my head. I just want hard breakage to not be possible. Why this
extra sort is necessary can be investigated by @Hiviexd if he's so
inclined.
## [Fix `GetClosestBeatDivisor()` not working correctly with negative
time
instants](https://github.com/ppy/osu/commit/88dc0d8a73c712a040635af262d7519fe6d772a0)
This was causing the 1/3 -> 1/2 conversion in Simplified Rhythm to
engage on some maps that weren't even mapped in waltz time. Those maps
had the common trait of having a timing point with a negative start
time.
Notably this could feasibly affect other places as well, like mania beat
snap colouring, or the Synesthesia osu! mod.
<details>
<summary>testing</summary>
Tested with Simplified Rhythm engaged, with 1/6 -> 1/4 and 1/3 -> 1/2
conversion enabled
[BULANOVA - NE PLACH'
[oni]](https://osu.ppy.sh/beatmapsets/1740291#taiko/3664995) (1/8 snap):
- No mods: 5.18*
- Simplified Rhythm @ master: 0.00*
- Simplified Rhythm @ f4807b3a: 3.48*
- Simplified Rhythm @ 933de7ab: 5.18*
[BULANOVA - NE PLACH' [inner
oni]](https://osu.ppy.sh/beatmapsets/1740291#taiko/3648625) (1/8 snap):
- No mods: 5.84*
- Simplified Rhythm @ master: 0.00*
- Simplified Rhythm @ f4807b3a: 4.13*
- Simplified Rhythm @ 933de7ab: 5.84*
[BULANOVA - NE PLACH' [don't
cry]](https://osu.ppy.sh/beatmapsets/1740291#taiko/3557681) (1/8 snap):
- No mods: 6.91*
- Simplified Rhythm @ master: 0.00*
- Simplified Rhythm @ f4807b3a: 4.84*
- Simplified Rhythm @ 933de7ab: 6.91*
[Mili - Peach Pit and Cyanide [nik's
Normal]](https://osu.ppy.sh/beatmapsets/2468654#osu/5405909) (1/6 snap):
- No mods: 1.63*
- Simplified Rhythm @ master: 0.00*
- Simplified Rhythm @ f4807b3a: 1.10*
- Simplified Rhythm @ 933de7ab: 1.10*
[Mili - Peach Pit and Cyanide [Ix's
Hard]](https://osu.ppy.sh/beatmapsets/2468654#osu/5405906) (1/3 snap):
- No mods: 2.50*
- Simplified Rhythm @ master: 0.00*
- Simplified Rhythm @ f4807b3a: 1.71*
- Simplified Rhythm @ 933de7ab: 1.71*
[Mili - Peach Pit and Cyanide [nomi's Hidden
Insane]](https://osu.ppy.sh/beatmapsets/2468654#osu/5405910) (1/3 snap):
- No mods: 3.93*
- Simplified Rhythm @ master: 0.00*
- Simplified Rhythm @ f4807b3a: 2.59*
- Simplified Rhythm @ 933de7ab: 2.59*
[Within Temptation - The Unforgiving [Stairway To The
Skies]](https://osu.ppy.sh/beatmapsets/29157#osu/172617) (1/3 snap in
editor):
- No mods: 1.53*
- Simplified Rhythm @ master: 0.00*
- Simplified Rhythm @ f4807b3a: 1.48*
- Simplified Rhythm @ 933de7ab: 1.48*
[Within Temptation - The Unforgiving
[Iron]](https://osu.ppy.sh/beatmapsets/29157#osu/172612) (1/6 snap in
editor):
- No mods: 2.76*
- Simplified Rhythm @ master: 0.00*
- Simplified Rhythm @ f4807b3a: 1.75*
- Simplified Rhythm @ 933de7ab: 1.75*
[Within Temptation - The Unforgiving
[Marathon]](https://osu.ppy.sh/beatmapsets/29157#osu/156352) (1/4 snap
in editor, but has 1/3 / 1/6 sections for sure):
- No mods: 2.96*
- Simplified Rhythm @ master: 0.00*
- Simplified Rhythm @ f4807b3a: 2.75*
- Simplified Rhythm @ 933de7ab: 2.75*
[Halozy - Genryuu Kaiko [Higan
Torrent]](https://osu.ppy.sh/beatmapsets/180138#osu/433005) (1/4 snap):
- No mods: 5.37*
- Simplified Rhythm @ master: 0.00*
- Simplified Rhythm @ f4807b3a: 3.95*
- Simplified Rhythm @ 933de7ab: 5.37*
</details>
Closes#33395
Copies the bookmarks from `referenceWorkingBeatmap` while creating a new
difficulty from scratch. I adapted the tests in
`TestSceneEditorBeatmapCreation` to include the bookmark checks.
---------
Co-authored-by: Dean Herbert <pe@ppy.sh>
Among others, this features a scary-looking change wherein
`WorkingBeatmap` now exposes a `RealmAccess` via
`IStorageResourceProvider`. This is necessary to make
`RealmBackedResourceStore` actually start firing callbacks when the
edited beatmap's skin is modified by adding new samples to it.
Even without the ID resetting logic before `Populate()` getting broken
and annotated with a TODO, this was kinda stupid. Why was purging logic
allowed to run *using a not-yet-completely-validated online ID of the
set*?
This is the other part of hopefully fixing scenarios like
https://osu.ppy.sh/community/forums/topics/2162457?n=8 (hopefully with
no babies poured out in the mean time, but only time will tell).
The end of `Populate()` is too early to do any validation of online IDs,
as population happens in `PostImport()` via `ProcessBeatmap`.
Additionally, this modifies the check to also include the set's ID. This
is one part of curtailing issues like
https://osu.ppy.sh/community/forums/topics/2162457?n=8 - the featured
artist template beatmap in question has a single difficulty with a bogus
positive beatmap set ID and a beatmap ID of 0, and my opinion is that
this sort of situation should for all intents and purposes cause the
beatmap set's ID to be reset.
A few facts of life:
- Guest difficulties are at this point a staple of mapping.
- People are very much used to flinging `.osu`s around (because there's
no better alternative).
- Currently there are two ways to get an `.osu` out of lazer. You can:
- Export the beatmap as "compatibility" to an `.osz`, then
transmogrify the `.osz` to a `.zip`, then extract the `.zip`, then
pluck out the `.osu`. This is the "correct" way to make sure stable
works, but is also stupidly arcane.
- Use "edit externally" to mount the beatmap files to disk, then
copy-paste out the `.osu`. This is the *wrong* way to make sure
stable works, because the mounting process exposes the raw "for
editing" format with features stable doesn't support, but it the
actual easy one.
- Reports about guest difficulties exported from lazer "working wrong on
stable" are prevalent. Probably mostly because of the preceding point.
What this PR does is introduce a *third* method to export an `.osu`,
which is designed to be both the easiest one yet *and* correct. I am
hoping this will curb the complaints until support for direct submission
of guest difficulties is added - which I still hope to see, but it will
be a significant effort *client-side* (the server side has been ready
for years now).
And yes, you will notice that much of the code added in
`LegacyBeatmapExporter` related to manipulation of the path is
copy-pasted from `LegacyExporter`. I don't care enough to invent
protected / abstract / whatever else OOP faff for something that may not
survive review and is mostly a weird semi-temporary wart.
Closes https://github.com/ppy/osu/issues/35807.
The reason this closes the aforementioned issue is as follows:
Taking https://osu.ppy.sh/beatmapsets/1236180#osu/4650477 as the
example, we have:
```
minBeatLength = 342.857142857143
maxBeatLength = 419.58041958042003
mostCommonBeatLength = 342.85700000000003
```
Note that `mostCommonBeatLength < minBeatLength` here.
Taking the inverse of that to compute BPM, we get
```
minBpm = 174.99999999999991
maxBpm = 142.99999999999986
mostCommonBpm = 175.00007291669704
```
which without DT present doesn't do anything bad, but when DT is
engaged (and thus BPM is multiplied by 1.5), midpoint rounding causes
the min BPM to become 262, and the most common BPM to become 263.
* Add failing test coverage for layered hit samples not playing in mania when beatmap is converted
Adding the `osu.Game.Rulesets.Osu` reference to the mania test project
is required so that `HitObjectSampleTest` base logic doesn't die on
https://github.com/ppy/osu/blob/f0aeeeea966f06add12cf2bca3dd48dac8573e82/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs#L88-L91
* Fix layered hit sounds not playing on converted beatmaps in mania
Compare
https://github.com/peppy/osu-stable-reference/blob/f9e58b4864a10f801393199e7652b2192c7342c3/osu!/GameplayElements/HitObjects/HitObject.cs#L476-L477.
In case of converted beatmaps, the last condition there
(`BeatmapManager.Current.PlayMode != PlayModes.OsuMania`) fails,
and thus layered hitsounds are allowed to play.
* Add failing test coverage for mania beatmap conversion assigning wrong samples to spinners
* Fix mania beatmap conversion assigning wrong samples to spinners
A spinner is never `IHasRepeats`. It was a dead condition, leading to
the hitobject generating fallback `NodeSamples`, which in particular
feature a silent tail which stable doesn't do.
Noticeably, stable also appears to force the head of the generated hold
note to have no addition sounds:
https://github.com/peppy/osu-stable-reference/blob/f9e58b4864a10f801393199e7652b2192c7342c3/osu!/GameplayElements/HitObjects/Mania/SpinnerMania.cs#L86-L89
* Add failing test coverage for file hit sample not falling back to plain samples if file missing
* Allow `FileHitSampleInfo` to fall back to standard samples if the file is not found (or not allowed to be looked up)
I'm honestly not 100% as to how closely this matches stable because I
reached the point wherein I'd rather not look at stable code anymore, so
as long as this passes tests I'm fine to wait for someone else to report
new breakage.
* Use alternative workaround for lack of osu! ruleset assembly in mania test project
* Fix encode stability test failures
Is this lazy? Sure it is. Friends and blocks do the same thing, though,
and I'm not overthinking this any more than I already have.
Being smarter here would likely mean being more invasive with respect to
listening in on all outgoing API requests and silently updating
favourites on that basis. Which is "smart" but also complicated.
This is a set of model changes which is supposed to facilitate support
for custom sample sets to the beatmap editor that is on par with stable.
It is the minimal set of changes. Because of this, it can probably be
considered "ugly" or however else you want to put it - but before you
say that, I want to try and pre-empt that criticism by explaining where
the problems lie.
Problem #1: duality in sample models
---
There is currently a weird duality of what a `HitObject`'s samples will
be.
- If an object has just been placed in the editor, and not saved /
decoded yet, it will use `HitSampleInfo`.
- If an object has already been encoded to the beatmap at least once, it
will use `ConvertHitObjectParser.LegacyHitSampleInfo`.
As long as that state of affairs remains, `HitSampleInfo` must be able
to represent anything that `LegacyHitSampleInfo` can, if feature parity
is to be achieved.
Problem 2: The 0 & 1 sample banks
---
Custom sample banks of 2 and above are a pretty clean affair. They map to
a suffix on the sample filename, and said samples are allowed to be
looked up from the beatmap skin. `Suffix` already exists in
`HitSampleInfo`.
However, the 1 custom sample bank is evil. It uses *non-suffixed*
samples, *allows lookups from the beatmap skins*, contrary to no bank /
bank 0, which *also* uses non-suffixed samples, but *doesn't* allow them
to be looked up from the beatmap skin.
This is why `HitSampleInfo.UseBeatmapSamples` has been called to
existence - without it there is no way to represent the ability of using
or not using the beatmap skin assets.
As has been stated previously in discussions about this feature, it's
both a *mapping* and a *skinning* concern.
There are many things you could do about either of these problems, but I
am pretty sure tackling either one is going to take *many* more lines of
code than this commit does. Which is why this is the starting point of
negotiation.
* SSV2 : Replace Mark as Played with Remove from Played if already played
* Remove checks of BeatmapInfo.LastPlayed for DateTimeOffset.MinValue
* Make FooterButtonOptions use a RealmLive<BeatmapInfo> and act on review comments
* FIXUP: Detach BeatmapInfo before passing it to FooterButtonOptions.Popover
---------
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
Alban Cabannes-Michel
·
2025-10-16 14:59:16 +02:00