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
- Skip the score in migration: treat an empty playable beatmap as non-migratable and leave its TotalScore unchanged, logging the beatmap/score.
- Verify the beatmap decodes to hit objects before invoking migration: pre-check working.Beatmap.HitObjects.Count and the converter output.
- 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
- Before score migration, verify the referenced beatmap decodes to hit objects for the score's ruleset.
- In bulk migration, isolate per-score failures so one bad beatmap doesn't abort the batch.
- Log the beatmap ID alongside skipped scores for later repair.
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
- Total score conversion operation returned invalid total of {
- Cannot recalculate score multiplier as TotalScoreWithoutMods
- Beatmap file {b.File!.Filename} could not be opened!
- Error when exporting {model.GetDisplayString()}: Multiple fi
- Timed out while loading beatmap ({beatmapInfo}).
AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13).
Data as JSON: /api/errors/65c33afa90100e49.
Report an issue: GitHub.