Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object placements with HR do not match stable (osu!catch) #27425

Open
1 of 3 tasks
Digitalfear117 opened this issue Feb 29, 2024 · 6 comments
Open
1 of 3 tasks

Object placements with HR do not match stable (osu!catch) #27425

Digitalfear117 opened this issue Feb 29, 2024 · 6 comments

Comments

@Digitalfear117
Copy link

Digitalfear117 commented Feb 29, 2024

Type

Game behaviour

Bug description

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
Coalamode on Stable Screenshot 1 Coalamode on Lazer Screenshot 1
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
Coalamode on Stable Screenshot 2 Coalamode on Lazer Screenshot 2
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
Coalamode Overlay 1 Coalamode Overlay 2

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.

Sewing.Machine.Lazer.vs.Stable.Short.MP4.mp4

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
Put an end Stable Screenshot 1 Put an end Lazer Screenshot 1
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
Put an end Stable Screenshot 2 Put an end Lazer Screenshot 2
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:

Put.an.end.video.comparison.h264.small.mp4
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
Stable screenshot on Colors with HR Lazer screenshot on Colors with HR
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
Colors Overlay

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

compressed-logs.zip

@frenzibyte
Copy link
Member

For the second example and the off-by-one juice streams, osu!stable truncates end time:

CleanShot 2024-03-02 at 02 20 41

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.

@Digitalfear117
Copy link
Author

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!

@bdach
Copy link
Collaborator

bdach commented Mar 15, 2024

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 #25725 etc.?

@frenzibyte frenzibyte removed their assignment Mar 18, 2024
@smoogipoo
Copy link
Contributor

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:

image

I'd hope that this is a separate issue or resolved by frenzi's change?

@bdach
Copy link
Collaborator

bdach commented Mar 20, 2024

I can't conclusively answer this either way at this time.

@Digitalfear117
Copy link
Author

Digitalfear117 commented Mar 20, 2024

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:
image
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:
image
This map is still exactly the same in the current build as it was before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Needs discussion
Development

No branches or pull requests

5 participants