ppy / osu

rhythm is just a *click* away!
https://osu.ppy.sh
MIT License
14.64k stars 2.16k forks source link

Slider tail volume is not saved properly #28587

Open TheMask3 opened 4 days ago

TheMask3 commented 4 days ago

Type

Game behaviour

Bug description

Try changing slider volumes in editor, while their volume changes are indeed applied when you replay the beatmap in editor, their volumes are not actually saved. After you save the beatmap, quit, and enter editor again, all slider volumes are reset.

(I also noticed an editor performance regression where osu would easily stall for a few hundred milliseconds when there's a slider on timeline while replaying beatmap in editor, the worst spike I've seen lasts more than 3 seconds but that's another topic...)

Screenshots or videos

https://github.com/ppy/osu/assets/13573720/c0f40c4e-d9ee-4968-a865-a797d4cedc88

https://github.com/ppy/osu/assets/13573720/53cfa815-ec99-4e27-ad02-767f95da9c96

Version

2024.625.0-lazer

Logs

compressed-logs.zip

bdach commented 4 days ago

Just to confirm that we're looking at the same thing: I can reproduce this, but only when changing the volume of the slider tail. The rest appears to work fine, and as far as I can see on your videos, the tail was the only thing that you were touching.

Is this correct? Is this the problem only with the volume of the tail, or are you seeing this for all slider node volumes and I need to keep looking?

TheMask3 commented 4 days ago

Just to confirm that we're looking at the same thing: I can reproduce this, but only when changing the volume of the slider tail. The rest appears to work fine, and as far as I can see on your videos, the tail was the only thing that you were touching.

Is this correct? Is this the problem only with the volume of the tail, or are you seeing this for all slider node volumes and I need to keep looking?

Yep, middle node volumes are saved and further tests show their volumes are copied to tails after undo.

bdach commented 4 days ago

Okay, that's little solace, but at least it's not as bad as I initially feared.

Reason for this happening is that the tail samples are not actually attached to any hitobject:

https://github.com/ppy/osu/blob/6cd1367329e09bb7c84fea65d304938aa67f179a/osu.Game.Rulesets.Osu/Objects/Slider.cs#L280-L284

Therefore, the encoding logic that converts per-object samples to stable control points / "green lines" does not see tail samples at all:

https://github.com/ppy/osu/blob/6cd1367329e09bb7c84fea65d304938aa67f179a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs#L281-L298

and thus ends up using the middle node volume.

The fix may be to just assign the samples to the tail hitobject after all, but ensure care that they don't play back at legacy last tick time as the relevant comment states...? @ppy/team-client @OliBomby thoughts, opinions?

OliBomby commented 3 days ago

The legacy encoder encodes sample information by putting control points on the end times of hitobjects and nested hitobjects. This is problematic for the the slider tail which coincides with the end time of the slider, because only one control point can exist at a time.

Also there is an asymmetry in the encoder/decoder that is partially responsible for this issue. The encoder recursively looks for samples in nested hitobjects while the decoder only looks at top-level hitobjects and checks if they inherit IHasRepeats (which is the more correct way to handle things imo since we cant trust nested hitobjects to have persistent state).

For a fix I think you have to solve this asymmetry and then implement a special offset for the last node sample so it doesnt overlap with the control point for the sliderbody samples.

bdach commented 3 days ago

The encoder recursively looks for samples in nested hitobjects while the decoder only looks at top-level hitobjects and checks if they inherit IHasRepeats (which is the more correct way to handle things imo since we cant trust nested hitobjects to have persistent state)

This is a fair shout actually. When investigating this I initially thought that that was weird and instinctively started writing the logic to encode NodeSamples for IHasRepeats instead, before catching myself and realising this normally is handled by nested objects.

The one slight problem with that is that you don't necessarily know when a node's time is. We can probably pretend it's not a problem and assume uniform duration distribution and be fine with using Duration / SpanCount()... until we're not (but that would probably only be a problem with something like a custom ruleset, I think for the main 4 rulesets it should be more than fine).

OliBomby commented 3 days ago

We can probably pretend it's not a problem and assume uniform duration distribution and be fine with using Duration / SpanCount()

We already made this assumption in many places like the repeats in the editor timeline or the legacy beatmap decoder. I think its safe even for custom rulesets, because the legacy format and hitobject interfaces lack the ability to encode non-uniform node samples.