ppy/osu · error · InvalidOperationException

Beatmap contains no hit objects!

Error message

Beatmap contains no hit objects!

What it means

Thrown by StandardisedScoreMigrationTools.convertFromLegacyTotalScore when, after calling beatmap.GetPlayableBeatmap, the resulting playableBeatmap.HitObjects.Count == 0. The simulator needs hit objects to compute legacy scoring attributes; an empty playable beatmap makes conversion impossible. Only reached for legacy scores on ILegacyRuleset rulesets.

Source

Thrown at osu.Game/Database/StandardisedScoreMigrationTools.cs:111

        /// <summary>
        /// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
        /// </summary>
        /// <param name="score">The score to convert the total score of.</param>
        /// <param name="ruleset">The <see cref="Ruleset"/> in which the score was set.</param>
        /// <param name="beatmap">The <see cref="WorkingBeatmap"/> applicable for this score.</param>
        /// <returns>The standardised total score.</returns>
        private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap)
        {
            if (!score.IsLegacyScore)
                return (score.TotalScoreWithoutMods, score.TotalScore);

            if (ruleset is not ILegacyRuleset legacyRuleset)
                return (score.TotalScoreWithoutMods, score.TotalScore);

            var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);

            if (playableBeatmap.HitObjects.Count == 0)
                throw new InvalidOperationException("Beatmap contains no hit objects!");

            ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
            LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap);
            var legacyBeatmapConversionDifficultyInfo = LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap);

            var mods = score.Mods;
            if (mods.Any(mod => mod is ModScoreV2))
                return ((long)Math.Round(score.TotalScore / sv1Simulator.GetLegacyScoreMultiplier(mods, legacyBeatmapConversionDifficultyInfo)), score.TotalScore);

            return convertFromLegacyTotalScore(score, ruleset, legacyBeatmapConversionDifficultyInfo, attributes);
        }

        /// <summary>
        /// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
        /// </summary>
        /// <param name="score">The score to convert the total score of.</param>
        /// <param name="ruleset">The <see cref="Ruleset"/> in which the score was set.</param>
        /// <param name="difficulty">The beatmap difficulty.</param>

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Skip the score in migration: treat an empty playable beatmap as non-migratable and leave its TotalScore unchanged, logging the beatmap/score.
  2. Verify the beatmap decodes to hit objects before invoking migration: pre-check working.Beatmap.HitObjects.Count and the converter output.
  3. Ensure the beatmap file used for the score is the correct mode/version; re-download or re-link the beatmap if its objects are missing.

Example fix

// before
var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);
if (playableBeatmap.HitObjects.Count == 0)
    throw new InvalidOperationException("Beatmap contains no hit objects!");

// after (skip non-migratable score)
var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);
if (playableBeatmap.HitObjects.Count == 0)
{
    LogForModel(score, "Beatmap has no hit objects; skipping legacy score conversion.");
    return (score.TotalScoreWithoutMods, score.TotalScore);
}
Defensive patterns

Strategy: validation

Validate before calling

var playable = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);
if (playable.HitObjects.Count == 0)
{ /* log + skip score migration, keep legacy TotalScore */ return; }

Type guard

static bool HasHitObjects(IBeatmap playable) => playable != null && playable.HitObjects.Count > 0;

Try / catch

try { convertFromLegacyTotalScore(score, ruleset, beatmap); }
catch (InvalidOperationException ex) when (ex.Message.Contains("no hit objects"))
{ /* mark score as non-migratable, continue batch */ }

Prevention

When it happens

Trigger: Migrating a legacy score whose beatmap, after ruleset conversion, yields zero hit objects — e.g. a beatmap that decodes to objects of a type the converter drops, a corrupt beatmap, or a ruleset/beatmap mode mismatch. The check sits right before Simulate().

Common situations: Bulk score migration runs hitting a beatmap file that is empty/corrupt or whose objects don't survive conversion for the score's ruleset; a beatmap that was deleted/edited after the score was set; conversion mods stripping all objects.

Related errors


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