{"record":{"id":"3bcd034ced6badc7","repo":"ppy/osu","slug":"a-hitobject-was-hit-before-it-became-hittable","errorCode":null,"errorMessage":"A {hitObject} was hit before it became hittable!","messagePattern":"A (.+?) was hit before it became hittable!","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"critical","filePath":"osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs","lineNumber":67,"sourceCode":"\r\n            // Generally when the user has hit way too early.\r\n            if (result == HitResult.None)\r\n                return ClickAction.Shake;\r\n\r\n            return ClickAction.Hit;\r\n        }\r\n\r\n        public void HandleHit(DrawableHitObject hitObject)\r\n        {\r\n            if (HitObjectContainer == null)\r\n                throw new InvalidOperationException($\"{nameof(HitObjectContainer)} should be set before {nameof(HandleHit)} is called.\");\r\n\r\n            // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).\r\n            if (!hitObjectCanBlockFutureHits(hitObject))\r\n                return;\r\n\r\n            if (CheckHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset, hitObject.Result.Type) != ClickAction.Hit)\r\n                throw new InvalidOperationException($\"A {hitObject} was hit before it became hittable!\");\r\n\r\n            // Miss all hitobjects prior to the hit one.\r\n            foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))\r\n            {\r\n                if (obj.Judged)\r\n                    continue;\r\n\r\n                if (hitObjectCanBlockFutureHits(obj))\r\n                    ((DrawableOsuHitObject)obj).MissForcefully();\r\n            }\r\n        }\r\n\r\n        /// <summary>\r\n        /// Whether a <see cref=\"HitObject\"/> blocks hits on future <see cref=\"HitObject\"/>s until its start time is reached.\r\n        /// </summary>\r\n        /// <param name=\"hitObject\">The <see cref=\"HitObject\"/> to test.</param>\r\n        private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject)\r\n            => hitObject is DrawableHitCircle;\r","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/ppy/osu/blob/d9c73e12adff2feaae4a3e158d36fe5883faf6ca/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs#L49-L85","documentation":"After a hit is registered, StartTimeOrderedHitPolicy.HandleHit() re-checks via CheckHittable to confirm the object was actually hittable at the hit time (startTime + TimeOffset). If CheckHittable returns anything other than ClickAction.Hit, the game's internal invariant is violated: an object was processed as a hit when the policy says it should not have been. This indicates a logic error in the hit pipeline, not a user mistake.","triggerScenarios":"A DrawableHitObject registers a hit judgement and calls HandleHit, but CheckHittable determines the object was not in a hittable state — e.g., a blocking object precedes it, or the hit time is before the object becomes hittable. This happens when the input pipeline processes a hit without first consulting the policy.","commonSituations":"Custom ruleset modifications to the hit pipeline that bypass the hit-policy check; mods that alter timing windows or hit ordering in ways inconsistent with the policy; multiplayer race conditions where hit times differ between clients; custom DrawableHitObject subclasses that call OnHit directly.","solutions":["Ensure the DrawableHitObject's input handling calls CheckHittable before processing the hit — if it returns non-Hit, do not register the hit.","Verify that no mod or custom DrawableHitObject bypasses the hit policy check before calling HandleHit.","Ensure HitObjectContainer ordering matches the start-time ordering expected by StartTimeOrderedHitPolicy.","If this fires in a custom ruleset, audit the entire hit-registration path from input event to HandleHit."],"exampleFix":"// before — HandleHit called without prior CheckHittable check\nif (result.IsHit)\n    policy.HandleHit(this);\n\n// after — verify hittable before handling\nif (result.IsHit && policy.CheckHittable(this, Time.Current, result.Type) == ClickAction.Hit)\n    policy.HandleHit(this);","handlingStrategy":"validation","validationCode":"// Verify the object is hittable before registering the hit\nvar clickAction = policy.CheckHittable(hitObject, hitObject.HitObject.StartTime + result.TimeOffset, result.Type);\nif (clickAction != ClickAction.Hit)\n    return; // do not process the hit\npolicy.HandleHit(hitObject);","typeGuard":null,"tryCatchPattern":"// Wrap HandleHit to catch the invariant violation during development\ntry\n{\n    policy.HandleHit(hitObject);\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"hit before it became hittable\"))\n{\n    // Log the full hit pipeline state for debugging\n    Logger.Log($\"Hit invariant violated for {hitObject}. Check hittability before registering hits.\", LoggingTarget.Runtime, LogLevel.Error);\n    throw;\n}","preventionTips":["Always call CheckHittable before registering a hit in custom DrawableHitObject subclasses.","Ensure HitObjectContainer ordering is consistent with StartTime ordering when using StartTimeOrderedHitPolicy.","Audit the full hit-registration path from input event to HandleHit when adding mods or custom hit objects.","Add integration tests that simulate early hits and verify the policy correctly blocks them at the input level."],"tags":["osu-ruleset","hit-policy","invariant","gameplay","debug","critical"],"backgroundTag":null,"analyzedSha":"d9c73e12adff2feaae4a3e158d36fe5883faf6ca","analyzedAt":"2026-08-13T14:12:54.015Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}