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

Correctly implement stable notelock in Classic mod #24280

Merged
merged 17 commits into from
Sep 1, 2023

Conversation

tybug
Copy link
Member

@tybug tybug commented Jul 18, 2023

As far as I can tell, stable notelock was never implemented correctly in lazer. This PR implements stable notelock effectively 1:1.

There are two obvious behavioral changes in this diff, though there may be more that are obscured by the details.

  • on lazer master, clicks on stacked hitobjects which are notelocked cause shakes, while on stable they don't (yes, despite the note being notelocked. stacks are special-cased on stable.)
  • stable has a weird extra 3 ms of leniency before hitobjects can be notelocked. Not sure why — the "unsnapped objects" comment doesn't make much sense to me — but it's back now.

Relevant stable code:

        internal virtual ClickAction CheckClickAction(HitObject checkObject)
        {
            if (checkObject.IsType(HitObjectType.Normal))
            {
                //check stack - ignore clicks on circles lower in stack.
                int index = hitObjectManager.hitObjectsMinimal.IndexOf(checkObject);

                if (index > 0 && hitObjectManager.hitObjectsMinimal[index - 1].StackCount > 0 &&
                    hitObjectManager.hitObjectsMinimal[index - 1].IsVisible &&
                    !hitObjectManager.hitObjectsMinimal[index - 1].IsHit)
                    return ClickAction.Ignore;
            }

            foreach (HitObject testObject in hitObjectManager.hitObjectsMinimal)
            {
                if (testObject.IsHit) continue;

                // if we found the object being checked, we can move on to the final timing test.
                if (testObject == checkObject)
                    break;

                // for all other objects, we check for validity and block the hit if any are still valid.
                // 3ms of extra leniency to account for slightly unsnapped objects.
                if (testObject.EndTime + 3 < checkObject.StartTime)
                    return ClickAction.Shake;
            }

            int autopilotAdjust = Player.Relaxing2 ? 200 : 0;
            return Math.Abs(checkObject.StartTime - AudioEngine.Time) < HitObjectManager.HITTABLE_RANGE - autopilotAdjust
                       ? ClickAction.Hit
                       : ClickAction.Shake;
        }

I'm very aware that a change this far reaching deserves tests...but I'm honestly not sure how to easily test this (particularly a note being shaken vs ignored). Pointers in that regard would be appreciated.

@@ -20,7 +20,7 @@ public interface IHitPolicy
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
/// <param name="time">The time to check.</param>
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
bool IsHittable(DrawableHitObject hitObject, double time);
ClickAction CheckHittable(DrawableHitObject hitObject, double time);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open to renamings of both CheckHittable and ClickAction. ClickAction was brought across from the stable naming.

yield break;
var previousHitObject = (DrawableOsuHitObject)HitObjectContainer.AliveObjects[index - 1];
if (previousHitObject.HitObject.StackHeight > 0 && !previousHitObject.AllJudged)
return ClickAction.Ignore;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this case right here is why the ClickAction abstraction is necessary. There are times when a note should, by all other metrics, be shaken, but we don't want it to be (e.g. stacks).

@tybug
Copy link
Member Author

tybug commented Jul 18, 2023

I see the test failures (was accidentally only running osu.Game.Tests locally). Will address when I wake. Don't submit PRs right before sleep! 😄

stable actually allows for hitobjs to be hit in the middle of sliders, as long as it doesn't interfere with the end time of the slider.
@tybug
Copy link
Member Author

tybug commented Jul 19, 2023

I've fixed one test in 15af852 by adjusting expected judgments to the correct notelock: believe it or not (I didn't, and had to test against stable), stable allows hitobjects to be hit even while a slider head is the primary / next object, as long as that hitobject doesn't interfere with the end time of the slider. I guess that's another stable mismatch bug fixed by this pull that I didn't even know about.

The other two failing tests are a result of custom hitwindows. Since this pull (and stable) hardcodes the notelock check at 400ms instead of using hitwindow information - which happens to also be 400ms for HitResult.Miss - the notelock is wrong relative to the test hitwindow.

On one hand, the obvious solution here seems to be to respect hitwindows in ObjectOrderedHitPolicy. However, if the default lazer hitwindows change in the future, the classic mod should still use continue using stable hitwindows at 400. Maybe in the latter case the hit policy should respect hitwindows, but the classic mod would be responsible for setting stable hitwindows game-wide. Just thinking about potential future footguns here if adding that is forgotten. Interested in thoughts here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for rename suggestions - maybe HitResponse or something instead? Maybe overthought. What you have is probably fine.

osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs Outdated Show resolved Hide resolved
@bdach
Copy link
Collaborator

bdach commented Jul 19, 2023

I'm very aware that a change this far reaching deserves tests...but I'm honestly not sure how to easily test this (particularly a note being shaken vs ignored). Pointers in that regard would be appreciated.

Tests would go a long way here yes. Loose sketch of how writing some may be possible (very untested, buyer beware):

  • Leverage existing setup in TestSceneObjectOrderedHitPolicy to get a player instance.
  • In order to observe the outputs of a hit policy, yank OsuPlayfield out of the player via ChildrenOfType<T>() and set its HitPolicy to an implementation which wraps ObjectOrderedHitPolicy and stores the results of CheckHittable to a list. Basically decorator pattern.
  • Use test setup as normal by setting up replay frames. At the end, rather than examine object judgements, check ClickActions stored to the list in the injected hit policy.

On one hand, the obvious solution here seems to be to respect hitwindows in ObjectOrderedHitPolicy. However, if the default lazer hitwindows change in the future, the classic mod should still use continue using stable hitwindows at 400. Maybe in the latter case the hit policy should respect hitwindows, but the classic mod would be responsible for setting stable hitwindows game-wide. Just thinking about potential future footguns here if adding that is forgotten. Interested in thoughts here.

Let's not try to future-proof too much and just hardcode the constant along with the rest of the legacy cruft. As far as I'm concerned we're not changing hitwindows anyway without a whole nother huge effort to somehow not break every leaderboard in existence. I don't even know why'd be changing hitwindows to be honest.

@tybug tybug changed the title Correctly implement stable notelock Correctly implement stable notelock in Classic mod Jul 20, 2023
@tybug
Copy link
Member Author

tybug commented Jul 20, 2023

Let's not try to future-proof too much and just hardcode the constant along with the rest of the legacy cruft.

Well, hardcoding the constant is what's causing the tests to fail, since the tests use custom hitwindows. Should I remove/adjust the tests?

@bdach
Copy link
Collaborator

bdach commented Jul 20, 2023

Well, hardcoding the constant is what's causing the tests to fail, since the tests use custom hitwindows

Oh. Right, I somehow had this backwards. In that case yeah adjusting the tests sounds best.

@bdach bdach self-assigned this Aug 22, 2023
@bdach
Copy link
Collaborator

bdach commented Aug 22, 2023

Just as a heads up: I have put a considerable amount of time into looking at what needs to happen to get this one merged, and I feel I've gathered enough to work with for one or two possible avenues of shipping this change. I'll leave this be for the time being, but tomorrow I'll come back to this with a fresh mind and hopefully I can get this unblocked and also provide testing for the behaviours this new implementation aims to deliver.

@tybug
Copy link
Member Author

tybug commented Aug 22, 2023

Thanks! To be honest, I've been putting off working on this because I know it'll take me a while to wrap my head around writing tests for it.

@bdach
Copy link
Collaborator

bdach commented Aug 23, 2023

I believe I've got a version of this branch that I would consider ready lined up, but I'm waiting for the reaction to #24627 and #24628 as I want to build this one on them, so I'd rather want to avoid force pushing them out if it turns out they aren't going to get approval.

The WIP version can be seen here, if anyone cares: https://github.com/ppy/osu/compare/master...bdach:osu:stable-notelock-working?expand=1 - if the two above pulls get merged then I'll push those changes in here.

@bdach
Copy link
Collaborator

bdach commented Aug 24, 2023

@ppy/team-client should be good for review now. @tybug would appreciate your cross-check as well that I haven't broken anything with my changes, if I can get it.

@@ -191,7 +198,9 @@ public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged()
addJudgementAssert(hitObjects[0], HitResult.Meh);
addJudgementAssert(hitObjects[1], HitResult.Meh);
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
addJudgementOffsetAssert(hitObjects[0], -90); // time_second_circle - first_circle_time - 90
addJudgementOffsetAssert(hitObjects[1], -190); // time_second_circle - first_circle_time - 90
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I accidentally broke this assertion in #24628, but it was broken before I touched the test scene (note that it used to check hitObjects[0] twice). I'm just fixing here to reduce overall ceremony.

@tybug
Copy link
Member Author

tybug commented Aug 25, 2023

I checked out this branch to test, but it seems to have a renderer-level crash for me when entering song select:

runtime.log

Stacktrace
2023-08-25 04:50:52 [verbose]: 🌅 Global background change queued
2023-08-25 04:50:53 [error]: An unhandled error has occurred.
2023-08-25 04:50:53 [error]: Veldrid.VeldridException: Error(s) were encountered during the execution of OpenGL commands. See InnerException for more information.
2023-08-25 04:50:53 [error]: ---> Veldrid.VeldridException: Error linking GL program: WARNING: Could not find vertex shader attribute 'm_TexturePosition' to match BindAttributeLocation request.
2023-08-25 04:50:53 [error]: WARNING: Could not find vertex shader attribute 'm_TextureRect' to match BindAttributeLocation request.
2023-08-25 04:50:53 [error]: WARNING: Could not find vertex shader attribute 'm_maskingIndex' to match BindAttributeLocation request.
2023-08-25 04:50:53 [error]: WARNING: Could not find vertex shader attribute 'm_backbufferDrawDepth' to match BindAttributeLocation request.
2023-08-25 04:50:53 [error]: ERROR: Input of fragment shader 'vdspv_fsin0' not written by vertex shader
2023-08-25 04:50:53 [error]: 
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLPipeline.CreateGraphicsGLResources()
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLPipeline.CreateGLResources()
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLCommandExecutor.ActivateGraphicsPipeline()
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLCommandExecutor.SetPipeline(Pipeline pipeline)
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.NoAllocEntryList.OpenGLNoAllocCommandEntryList.ExecuteAll(OpenGLCommandExecutor executor)
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLGraphicsDevice.ExecutionThread.ExecuteWorkItem(ExecutionThreadWorkItem workItem)
2023-08-25 04:50:53 [error]: --- End of inner exception stack trace ---
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLGraphicsDevice.ExecutionThread.CheckExceptions()
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLGraphicsDevice.ExecutionThread.WaitForIdle()
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLGraphicsDevice.SwapBuffersCore(Swapchain swapchain)
2023-08-25 04:50:53 [error]: at osu.Framework.Platform.GameHost.Swap()
2023-08-25 04:50:53 [error]: at osu.Framework.Platform.MacOS.MacOSGameHost.Swap()
2023-08-25 04:50:53 [error]: at osu.Framework.Platform.GameHost.DrawFrame()
2023-08-25 04:50:53 [error]: at osu.Framework.Threading.GameThread.processFrame()
2023-08-25 04:50:53 [error]: An unhandled error has occurred. (inner)
2023-08-25 04:50:53 [error]: Veldrid.VeldridException: Error linking GL program: WARNING: Could not find vertex shader attribute 'm_TexturePosition' to match BindAttributeLocation request.
2023-08-25 04:50:53 [error]: WARNING: Could not find vertex shader attribute 'm_TextureRect' to match BindAttributeLocation request.
2023-08-25 04:50:53 [error]: WARNING: Could not find vertex shader attribute 'm_maskingIndex' to match BindAttributeLocation request.
2023-08-25 04:50:53 [error]: WARNING: Could not find vertex shader attribute 'm_backbufferDrawDepth' to match BindAttributeLocation request.
2023-08-25 04:50:53 [error]: ERROR: Input of fragment shader 'vdspv_fsin0' not written by vertex shader
2023-08-25 04:50:53 [error]: 
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLPipeline.CreateGraphicsGLResources()
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLPipeline.CreateGLResources()
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLCommandExecutor.ActivateGraphicsPipeline()
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLCommandExecutor.SetPipeline(Pipeline pipeline)
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.NoAllocEntryList.OpenGLNoAllocCommandEntryList.ExecuteAll(OpenGLCommandExecutor executor)
2023-08-25 04:50:53 [error]: at Veldrid.OpenGL.OpenGLGraphicsDevice.ExecutionThread.ExecuteWorkItem(ExecutionThreadWorkItem workItem)
2023-08-25 04:50:53 [verbose]: ⚠️ An unhandled error has occurred.
2023-08-25 04:50:53 [verbose]: Unhandled exception has been denied .

Does not occur with metal renderer. I'm on macos but am using opengl temporarily because of #23970 (which seems to be partially resolved by now; no startup crash anymore, at least).

I'm really confused here, because this does not occur on latest master, but does on this branch consistently (n=4 retries each). I can't see what changes in this branch are related to the renderer, which is why I'm hesitant to even post this here...have I hit a freak probabilistic crash that only appears to occur on this pr?

@bdach
Copy link
Collaborator

bdach commented Aug 25, 2023

There should be no reason why this branch specifically would trigger any sort of renderer level crash. Especially not on song select of all places. That's what I'd like to think, at least.

@tybug
Copy link
Member Author

tybug commented Aug 25, 2023

There should be no reason why this branch specifically would trigger any sort of renderer level crash.

That's what I figured. Looked into it more and I forgot to sync my fork ;-; so it is actually crashing on master as well. I'll open another issue for that.

@tybug
Copy link
Member Author

tybug commented Aug 25, 2023

I've tested the replays linked in the PR description and one other problematic one, and all behave as I would expect, so things look good purely from a visual perspective.

Two comments, though - first, I don't think there's a test for the 3ms of leniency. This was one of the things that I didn't explicitly test myself with a replay, though I can't see why it wouldn't work as expected. If it's judged that case isn't worth a test then that's of course fine with me.

A separate comment is that with this PR, replay 1 in the description - which is an FC - consistently breaks combo right around ~2100x (~5:18), but only when fast-forwarding to a point past it. This doesn't happen on master. I believe there's still various "fast-forward playback can be wrong" issues, so this probably isn't a blocker, but I'll mention it regardless since it's new behavior introduced here.

@bdach
Copy link
Collaborator

bdach commented Aug 25, 2023

first, I don't think there's a test for the 3ms of leniency.

Yeah there isn't. I decided not to bother with that one. Up to reviewers whether that's an issue I guess. It shouldn't be very difficult to come up with one if required.

replay 1 in the description - which is an FC - consistently breaks combo right around ~2100x (~5:18), but only when fast-forwarding to a point past it

I think we'd be looking to address that separately anyhow. The core takeaway is that it is more correct at 1x.

@bdach bdach requested a review from a team August 31, 2023 22:17
@peppy peppy self-requested a review September 1, 2023 07:51
peppy
peppy previously approved these changes Sep 1, 2023
@peppy peppy merged commit 8d0f6df into ppy:master Sep 1, 2023
17 checks passed
@tybug tybug deleted the stable-notelock branch September 4, 2023 04:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

Stacked Sliders behave differently with classic mod than how they do in stable
3 participants