osu
osu copied to clipboard
Object placements with HR do not match stable (osu!catch)
Type
Game behaviour
Bug description
- [x] https://github.com/ppy/osu/pull/27456 (for beatmap example 1)
- [ ] https://github.com/ppy/osu/issues/27425#issuecomment-1974104875 (for beatmap example 2)
- [ ] still going through this...
When playing HR on Lazer, many maps have objects that are in noticeably different spots compared to with Stable. This most likely is due to rounding differences between stable and lazer for JuiceStream velocity calculations seen in pull request 25725. Object randomization relies on the exact tiny droplet patterns generated throughout the map to get the randomization seed, if droplets are not the same, objects will not be in the same spot.
User Crafterdark created a parser version that allows you to generate the same json format for Stable ConversionMappings, but for Lazer. This can be used for quickly checking between the two files for a given timestamp, instead of running the test every time (especially since the test uses a leniency). I will be using that to show how off the objects are from stable. The tool can be found here. For our testing, we set the lenience = 3, because the misplacement grows so large on certain HR maps that it is no longer avoidable or unnoticeable in gameplay. This also means that the difficulty is no longer the same between the two games, as anywhere from a few, to many objects may be on entire different ends of the screen.
At BPM's 75, 150, 300, 600, etc, or close to these, precision loss is even higher than normal. If the map is one of these BPM's then almost every JuiceStream pattern has a high chance to have differences between the two versions of the game. In stable objects read from the .osu will consistently start either 1 ms early, or on time. The pattern of which is determined by the method stable uses to round double and float values. Since droplets can only generate when its been 101ms since a previous tick, this slight drifting of objects can cause droplets to generate or not generate sometimes. Lazer on the other hand does not have this object start time drifting, which is why we are seeing differences.
The amount of tiny droplet in "2 to the power of n minus 1", where n is the amount of division by 2 for each step. It depends only on the 101ms cutoff.
At n = 0, you have no tiny droplet
At n = 1, you have one tiny droplet
At n = 2, you have three tiny droplet
At n = 3, you have seven tiny droplet
(droplets only generate in patterns of 0, 1, 3, 7, etc)
If one of these 2 steps mismatch between Stable and Lazer => Different map
We are unsure exactly where this precision loss occurs in stable, so proposing fixes to this solution has been very difficult. The only proposed fix we can suggest to do is crosscheck stable, and ensure that the precision losses and the rest of the values are set correctly for object placement. (Call this a legacy generation, since it's full of precision losses that are inaccurate). However, you guys may be able to come up with a better solution!
It does seem that as the star rating increases, the more likely it becomes for maps to have object randomization issues. But it can happen to maps of any star rating, whether it's a map made specifically for CtB, or a map converted from Standard to CtB.
I also want to mention that although not every map has combo increasing notes being displaced (circles, or sliderheads/slidertails) on lazer with HR, the position of tiny droplets are slightly different on lazer for a significant portion of maps in the game.
Lastly, huge shout outs to Crafterdark for helping with this write-up, it wouldn't have been possible without him 💪
1st example: Aru's Cup on Coalamode. - Nanairo Symphony -TV Size- Beatmap ID: 1041052 Beatmap URL: https://osu.ppy.sh/beatmapsets/488149#fruits/1041052
Stable HR SR | Lazer HR SR | Number of Objects In Different Spots |
---|---|---|
1.75* | 1.77* | 3 |
Stable at ~0:43 | Lazer at ~0:43 |
---|---|
![]() |
![]() |
Expected: {"StartTime":45284.0,"Position":56.0,"HyperDash":false} | Received: {"StartTime":45284.0,"Position":112.0,"HyperDash":false} |
Expected: {"StartTime":45671.0,"Position":264.0,"HyperDash":false} | Received: {"StartTime":45671.0,"Position":208.0,"HyperDash":false} |
Stable at ~0:51 | Lazer at ~0:51 |
---|---|
![]() |
![]() |
Expected: {"StartTime":53025.0,"Position":88.0,"HyperDash":false} | Received: {"StartTime":53025.0,"Position":176.0,"HyperDash":false} |
View of the two versions layered on top of each other, to make the differences more obvious:
Example 1 ~0:43 | Example 2 ~0:51 |
---|---|
![]() |
![]() |
In addition to these objects being in the wrong spots, objects also commonly appear -1ms off where they do in stable. You can read the full list of objects that are either off in starting time or position in this file created by Crafterdark's modified ConversationMappings parser
3 objects being off is pretty bad, it means replays set on stable won't work properly on lazer, and replays set on lazer won't work properly on stable. I also wanted to show that this happens to maps of all difficulties. And even if notes are not in the wrong spot, droplets in sliders are wrong on an even higher portion of maps compared to stable. Unfortunately, the this issue gets worse the harder the map is. This next map has 40% of objects being in different spots!
**2nd example: Madness on yak_won - Sewing Machine Beatmap ID: 988072 Beatmap URL: https://osu.ppy.sh/beatmapsets/461353#fruits/988072
Stable HR SR | Lazer HR SR | Number of Objects In Different Spots |
---|---|---|
9.82* | 10.16* | 123 |
I decided that screenshots were not going to suffice for this example, so here is a short clip with the two versions of osu! overlaid on top of each other.
https://github.com/ppy/osu/assets/28713262/1f2b3fa1-7de7-43eb-b124-7f3143a0aa7e
For a full list of objects that are appearing at the wrong time, or the wrong position, you can refer to this zip file created by Crafterdark's parser.
This issue does have more layers, as some maps have incorrect object placements even with NM, so that means with HR they are even more off compared to stable with HR applied, such as our third example
**3rd example: Revolt from the Abyss on Noah - Deadly force - Put an end Beatmap ID: 3172816 Beatmap URL: https://osu.ppy.sh/beatmapsets/1552869#fruits/3172816
Stable NM SR | Lazer NM SR | Number of Objects In Different Spots |
---|---|---|
9.48* | 9.49* | 49 |
Stable HR SR | Lazer HR SR | Number of Objects In Different Spots |
---|---|---|
10.33* | 10.34* | 48 |
With this map with HR, all patterns are in basically the same spot till we reach the time stamp of 295389.0, where stable generates 3 droplets in between the slider tick, but lazer only generates a single droplet at this time.
Stable at ~4:49 | Lazer at ~4:49 |
---|---|
Expected: {"StartTime":295389.0,"Position":156.035,"HyperDash":false} | Received: {"StartTime":295440.0,"Position":150.38306,"HyperDash":false} |
Expected: {"StartTime":295739.0,"Position":100.54614,"HyperDash":false} | The conversion did not generate a hitobject, but should have, for hitobject at time: 294540 |
Expected: {"StartTime":295840.0,"Position":75.88822,"HyperDash":false} | The conversion did not generate a hitobject, but should have, for hitobject at time: 294540 |
Unfortunately, every following droplet ends up in different spots until the map reaches it's end time. In addition, the banana shower at the end (spinner), is also completely different because of it.
Stable Position ~5:04 | Lazer Position ~5:04 |
---|---|
Full list of every object in the spinner that is at the wrong position compared to Stable
Stable starting at ~5:04 | Lazer starting at ~5:04 |
---|---|
Expected: {"StartTime":310240.0,"Position":371.0,"HyperDash":false} | Received: {"StartTime":310240.0,"Position":16.74089,"HyperDash":false} |
Expected: {"StartTime":310296.0,"Position":293.0,"HyperDash":false} | Received: {"StartTime":310296.25,"Position":248.44305,"HyperDash":false} |
Expected: {"StartTime":310352.0,"Position":104.0,"HyperDash":false} | Received: {"StartTime":310352.5,"Position":100.85421,"HyperDash":false} |
Expected: {"StartTime":310408.0,"Position":194.0,"HyperDash":false} | Received: {"StartTime":310408.75,"Position":24.537123,"HyperDash":false} |
Expected: {"StartTime":310465.0,"Position":234.0,"HyperDash":false} | Received: {"StartTime":310465.0,"Position":66.82564,"HyperDash":false} |
Expected: {"StartTime":310521.0,"Position":179.0,"HyperDash":false} | Received: {"StartTime":310521.25,"Position":97.38554,"HyperDash":false} |
Expected: {"StartTime":310577.0,"Position":278.0,"HyperDash":false} | Received: {"StartTime":310577.5,"Position":267.34024,"HyperDash":false} |
Expected: {"StartTime":310633.0,"Position":474.0,"HyperDash":false} | Received: {"StartTime":310633.75,"Position":116.205284,"HyperDash":false} |
Expected: {"StartTime":310690.0,"Position":50.0,"HyperDash":false} | Received: {"StartTime":310690.0,"Position":451.5478,"HyperDash":false} |
Expected: {"StartTime":310746.0,"Position":458.0,"HyperDash":false} | Received: {"StartTime":310746.25,"Position":414.1756,"HyperDash":false} |
Expected: {"StartTime":310802.0,"Position":425.0,"HyperDash":false} | Received: {"StartTime":310802.5,"Position":88.95756,"HyperDash":false} |
Expected: {"StartTime":310858.0,"Position":466.0,"HyperDash":false} | Received: {"StartTime":310858.75,"Position":257.85693,"HyperDash":false} |
Expected: {"StartTime":310915.0,"Position":56.0,"HyperDash":false} | Received: {"StartTime":310915.0,"Position":175.06075,"HyperDash":false} |
Expected: {"StartTime":310971.0,"Position":109.0,"HyperDash":false} | Received: {"StartTime":310971.25,"Position":38.951332,"HyperDash":false} |
Expected: {"StartTime":311027.0,"Position":482.0,"HyperDash":false} | Received: {"StartTime":311027.5,"Position":283.61685,"HyperDash":false} |
Expected: {"StartTime":311083.0,"Position":147.0,"HyperDash":false} | Received: {"StartTime":311083.75,"Position":138.07207,"HyperDash":false} |
Expected: {"StartTime":311140.0,"Position":285.0,"HyperDash":false} | Received: {"StartTime":311140.0,"Position":102.145996,"HyperDash":false} |
Expected: {"StartTime":311196.0,"Position":452.0,"HyperDash":false} | Received: {"StartTime":311196.25,"Position":494.07382,"HyperDash":false} |
Expected: {"StartTime":311252.0,"Position":419.0,"HyperDash":false} | Received: {"StartTime":311252.5,"Position":54.913254,"HyperDash":false} |
Expected: {"StartTime":311308.0,"Position":269.0,"HyperDash":false} | Received: {"StartTime":311308.75,"Position":29.14941,"HyperDash":false} |
Expected: {"StartTime":311365.0,"Position":249.0,"HyperDash":false} | Received: {"StartTime":311365.0,"Position":69.43052,"HyperDash":false} |
Expected: {"StartTime":311421.0,"Position":233.0,"HyperDash":false} | Received: {"StartTime":311421.25,"Position":110.0262,"HyperDash":false} |
Expected: {"StartTime":311477.0,"Position":449.0,"HyperDash":false} | Received: {"StartTime":311477.5,"Position":167.15698,"HyperDash":false} |
Expected: {"StartTime":311533.0,"Position":411.0,"HyperDash":false} | Received: {"StartTime":311533.75,"Position":56.166637,"HyperDash":false} |
Expected: {"StartTime":311590.0,"Position":75.0,"HyperDash":false} | Received: {"StartTime":311590.0,"Position":10.146959,"HyperDash":false} |
Expected: {"StartTime":311646.0,"Position":474.0,"HyperDash":false} | Received: {"StartTime":311646.25,"Position":308.95013,"HyperDash":false} |
Expected: {"StartTime":311702.0,"Position":176.0,"HyperDash":false} | Received: {"StartTime":311702.5,"Position":288.25006,"HyperDash":false} |
Expected: {"StartTime":311758.0,"Position":1.0,"HyperDash":false} | Received: {"StartTime":311758.75,"Position":57.25569,"HyperDash":false} |
Expected: {"StartTime":311815.0,"Position":37.0,"HyperDash":false} | Received: {"StartTime":311815.0,"Position":258.17734,"HyperDash":false} |
Expected: {"StartTime":311871.0,"Position":481.0,"HyperDash":false} | Received: {"StartTime":311871.25,"Position":180.98752,"HyperDash":false} |
Expected: {"StartTime":311927.0,"Position":375.0,"HyperDash":false} | Received: {"StartTime":311927.5,"Position":198.62968,"HyperDash":false} |
Expected: {"StartTime":311983.0,"Position":407.0,"HyperDash":false} | Received: {"StartTime":311983.75,"Position":211.70355,"HyperDash":false} |
Expected: {"StartTime":312040.0,"Position":231.0,"HyperDash":false} | Received: {"StartTime":312040.0,"Position":503.37738,"HyperDash":false} |
Video clip of the end of the map. Both versions of the game overlaid on top of each other:
https://github.com/ppy/osu/assets/28713262/f1bf4343-2d04-4c84-ad94-ecc70600e165
Stable starting at ~4:52 | Lazer starting at ~4:52 |
---|---|
Expected: {"StartTime":298540.0,"Position":74.60267,"HyperDash":false} | Received: {"StartTime":298540.0,"Position":107.5,"HyperDash":false} |
Expected: {"StartTime":298921.0,"Position":105.691154,"HyperDash":false} | Received: {"StartTime":298922.0000228882,"Position":87.57,"HyperDash":false} |
Expected: {"StartTime":299340.0,"Position":290.2943,"HyperDash":false} | Received: {"StartTime":299340.0,"Position":300.5,"HyperDash":false} |
Expected: {"StartTime":302297.0,"Position":104.37605,"HyperDash":false} | Received: {"StartTime":302297.0,"Position":121.26424,"HyperDash":false} |
Expected: {"StartTime":308697.0,"Position":27.290276,"HyperDash":false} | Received: {"StartTime":308697.0,"Position":5.1294174,"HyperDash":false} |
Expected: {"StartTime":308897.0,"Position":284.74,"HyperDash":false} | Received: {"StartTime":308897.0,"Position":302.74,"HyperDash":false} |
Expected: {"StartTime":309197.0,"Position":413.70972,"HyperDash":false} | Received: {"StartTime":309197.0,"Position":427.87057,"HyperDash":false} |
Expected: {"StartTime":309839.0,"Position":29.739292,"HyperDash":false} | Received: {"StartTime":309840.0,"Position":33.270416,"HyperDash":false} |
Here again are the tiny droplets that are not being generated compared to stable. If these were to generate the amount of issues with this map should disappear.
The conversion did not generate a hitobject, but should have, for hitobject at time: 294540:
Expected: {"StartTime":295739.0,"Position":100.54614,"HyperDash":false}
The conversion did not generate a hitobject, but should have, for hitobject at time: 294540:
Expected: {"StartTime":295840.0,"Position":75.88822,"HyperDash":false}
For a full list of objects that are appearing at the wrong time, or the wrong position, you can refer to this zip file for NM and then this zip file for HR created by Crafterdark's parser.
I wanted to show an example of a standard convert having the issue, and also an example where a score exists in lazer. This score is also the reason I began looking into this issue
Score URL: https://osu.ppy.sh/scores/2352883924 Lazer score ID: 2352883924
**4th example: Hard on dj TAKA - Colors -sasakure.UK Futurelogic Remix- Beatmap ID: 1367640 Beatmap URL: https://osu.ppy.sh/beatmapsets/317439#fruits/1367640
Stable HR SR | Lazer HR SR | Number of Objects In Different Spots |
---|---|---|
2.97* | 2.96* | 2 |
Stable at ~1:52 | Lazer at ~1:52 |
---|---|
Expected: {"StartTime":138012.0,"Position":100.0,"HyperDash":false} | Received: {"StartTime":138012.0,"Position":200.0,"HyperDash":false} |
Expected: {"StartTime":138212.0,"Position":172.0,"HyperDash":false} | Received: {"StartTime":138212.0,"Position":144.0,"HyperDash":false} |
Both versions overlaid at ~1:52 |
---|
Because of these differences, replays from both versions of the games cannot work correctly between each other. Not only that, but the difficulty, SR, and PP cannot be the same for many maps in this current state.
Also, sliders ending 1ms short happens with the other game modes too most likely.
Screenshots or videos
No response
Version
2024.221.0 and many versions before this
Logs
For the second example and the off-by-one juice streams, osu!stable truncates end time:
On lazer, the end time appears perfectly as 1918 (start time = 1843, path distance = 60, velocity = 0.8, therefore end time = 1843 + 60 / 0.8 = 1918).
I'm not entirely sure how we can support this, and after sitting down and going through stable code, I cannot find a specific point at which I can emulate stable's behaviour.
The closest I have reached is that, with this custom beatmap, specifically a juice stream with p1 = (0, 0) and p2 = (24, -64), stable calculates distance as 59.9999962, meanwhile lazer calculates distance as 60.0000038 and overwrites it by the ExpectedDistance
field which is just 60). The difference in calculation, alongside the fact that stable truncates end time, causes juice stream duration to become 74ms in stable and 75ms in lazer.
The issue with beatmap example 4 does appear to be fixed by #27456 as well, so thats good. Issues with beatmap example 2 and 3 still occur, but thats not shocking. Either way, one step closer to full compatibility!
I looked at the remaining cases briefly today but it's a bit grim.
I am pretty sure that the reason for the discrepancy in the calculated path length between stable and lazer is caused by catastrophic cancellation. lazer's cumulative path calculation relies on SliderPath
which is expected to be anchored at (0, 0), while stable calculates cumulative path length using playfield-space coordinates. This can ever so slightly skew the length of a single segment:
new Vector2(240, 152) + Path.PositionAt(1) - new Vector2(240, 152)
{(-18.095657, 67.620605)}
Length: 69.9999924
LengthFast: 70.00103
LengthSquared: 4899.99902
PerpendicularLeft: {(-67.620605, -18.095657)}
PerpendicularRight: {(67.620605, 18.095657)}
X: -18.0956573
Y: 67.6206055
Yx: {(67.620605, -18.095657)}
Path.PositionAt(1)
{(-18.095655, 67.62061)}
Length: 70
LengthFast: 70.0010376
LengthSquared: 4900
PerpendicularLeft: {(-67.62061, -18.095655)}
PerpendicularRight: {(67.62061, 18.095655)}
X: -18.0956554
Y: 67.6206131
Yx: {(67.62061, -18.095655)}
and across many segments, this gets obviously worse.
The obvious thing would be to attempt to simulate this by just mirroring stable, but this is not currently doable because the hitobject conversion process for catch discards half of the information required here (namely, the Y position of the object pre-conversion). And changing that will not only be a whole lot of work, it also feels very stupid to do just for the sake of stable parity.
@smoogipoo do you have any thoughts to offer here as someone who's spent some time on these sorts of problems already in https://github.com/ppy/osu/pull/25725 etc.?
That indeed looks pretty grim and I don't have and immediate ideas, however this example in the 2nd example looks much more serious and occurs over shorter distances:
I'd hope that this is a separate issue or resolved by frenzi's change?
I can't conclusively answer this either way at this time.
Hi @smoogipoo @bdach, I have taken a look at the sewing machine example once more, and it does seem that this is fixed, here is proof:
I checked the rest of this map, and visually it looks the same as stable, although the star rating is still reported as 10.16* in lazer with HR, not 9.82* as stable does.
However the example of Deadly force - Put an end is not resolved. specifically, whatever creates the two additional droplets on this slider in stable still is not resolved in lazer (this means the randomization of all droplets after this point is incorrect, and the spinner is incorrect) Here is proof:
This map is still exactly the same in the current build as it was before.