ppy/osu · error · InvalidOperationException

HitObjectContainer should be set before CheckHittable is cal

Error message

HitObjectContainer should be set before CheckHittable is called.

What it means

StartTimeOrderedHitPolicy.CheckHittable() requires HitObjectContainer to enumerate hit objects up to the target's start time and determine if a blocking object precedes it. The policy must be initialized with HitObjectContainer before hit-checking. This mirrors the LegacyHitPolicy invariant but in a different policy implementation.

Source

Thrown at osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs:29

namespace osu.Game.Rulesets.Osu.UI
{
    /// <summary>
    /// Ensures that <see cref="HitObject"/>s are hit in-order of their start times. Affectionately known as "note lock".
    /// If a <see cref="HitObject"/> is hit out of order:
    /// <list type="number">
    /// <item><description>The hit is blocked if it occurred earlier than the previous <see cref="HitObject"/>'s start time.</description></item>
    /// <item><description>The hit causes all previous <see cref="HitObject"/>s to missed otherwise.</description></item>
    /// </list>
    /// </summary>
    public class StartTimeOrderedHitPolicy : IHitPolicy
    {
        public IHitObjectContainer? HitObjectContainer { get; set; }

        public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result)
        {
            if (HitObjectContainer == null)
                throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called.");

            DrawableHitObject? blockingObject = null;

            foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
            {
                if (hitObjectCanBlockFutureHits(obj))
                    blockingObject = obj;
            }

            if (blockingObject != null)
            {
                // A hit is disallowed if:
                // 1. The last blocking hitobject has not yet been judged.
                // 2. The current time is before the last hitobject's start time.
                //
                // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
                if (!blockingObject.Judged && time < blockingObject.HitObject.StartTime)
                    return ClickAction.Shake;

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Set HitObjectContainer on the policy before gameplay starts.
  2. Ensure the policy initialization happens in the playfield's load step, consistently for all policy implementations.

Example fix

// before
var policy = new StartTimeOrderedHitPolicy();
policy.CheckHittable(hitObject, time, result); // HitObjectContainer is null

// after
var policy = new StartTimeOrderedHitPolicy();
policy.HitObjectContainer = HitObjectContainer;
policy.CheckHittable(hitObject, time, result);
Defensive patterns

Strategy: validation

Validate before calling

// Ensure HitObjectContainer is set before gameplay
if (policy.HitObjectContainer == null)
    throw new InvalidOperationException("HitObjectContainer must be set before gameplay.");
policy.HitObjectContainer = HitObjectContainer;

Prevention

When it happens

Trigger: Calling CheckHittable before setting HitObjectContainer on the StartTimeOrderedHitPolicy instance.

Common situations: Switching from LegacyHitPolicy to StartTimeOrderedHitPolicy without ensuring the same initialization is performed; test setup omission; ruleset configuration that creates a policy lazily but queries it eagerly.

Related errors


AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13). Data as JSON: /api/errors/d4cf4c700a936d4c. Report an issue: GitHub.