{"record":{"id":"65c33afa90100e49","repo":"ppy/osu","slug":"beatmap-contains-no-hit-objects","errorCode":null,"errorMessage":"Beatmap contains no hit objects!","messagePattern":"Beatmap contains no hit objects!","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"osu.Game/Database/StandardisedScoreMigrationTools.cs","lineNumber":111,"sourceCode":"        /// <summary>\r\n        /// Converts from <see cref=\"ScoreInfo.LegacyTotalScore\"/> to the new standardised scoring of <see cref=\"ScoreProcessor\"/>.\r\n        /// </summary>\r\n        /// <param name=\"score\">The score to convert the total score of.</param>\r\n        /// <param name=\"ruleset\">The <see cref=\"Ruleset\"/> in which the score was set.</param>\r\n        /// <param name=\"beatmap\">The <see cref=\"WorkingBeatmap\"/> applicable for this score.</param>\r\n        /// <returns>The standardised total score.</returns>\r\n        private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap)\r\n        {\r\n            if (!score.IsLegacyScore)\r\n                return (score.TotalScoreWithoutMods, score.TotalScore);\r\n\r\n            if (ruleset is not ILegacyRuleset legacyRuleset)\r\n                return (score.TotalScoreWithoutMods, score.TotalScore);\r\n\r\n            var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);\r\n\r\n            if (playableBeatmap.HitObjects.Count == 0)\r\n                throw new InvalidOperationException(\"Beatmap contains no hit objects!\");\r\n\r\n            ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();\r\n            LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap);\r\n            var legacyBeatmapConversionDifficultyInfo = LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap);\r\n\r\n            var mods = score.Mods;\r\n            if (mods.Any(mod => mod is ModScoreV2))\r\n                return ((long)Math.Round(score.TotalScore / sv1Simulator.GetLegacyScoreMultiplier(mods, legacyBeatmapConversionDifficultyInfo)), score.TotalScore);\r\n\r\n            return convertFromLegacyTotalScore(score, ruleset, legacyBeatmapConversionDifficultyInfo, attributes);\r\n        }\r\n\r\n        /// <summary>\r\n        /// Converts from <see cref=\"ScoreInfo.LegacyTotalScore\"/> to the new standardised scoring of <see cref=\"ScoreProcessor\"/>.\r\n        /// </summary>\r\n        /// <param name=\"score\">The score to convert the total score of.</param>\r\n        /// <param name=\"ruleset\">The <see cref=\"Ruleset\"/> in which the score was set.</param>\r\n        /// <param name=\"difficulty\">The beatmap difficulty.</param>\r","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/ppy/osu/blob/d9c73e12adff2feaae4a3e158d36fe5883faf6ca/osu.Game/Database/StandardisedScoreMigrationTools.cs#L93-L129","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","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."],"exampleFix":"// before\nvar playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);\nif (playableBeatmap.HitObjects.Count == 0)\n    throw new InvalidOperationException(\"Beatmap contains no hit objects!\");\n\n// after (skip non-migratable score)\nvar playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);\nif (playableBeatmap.HitObjects.Count == 0)\n{\n    LogForModel(score, \"Beatmap has no hit objects; skipping legacy score conversion.\");\n    return (score.TotalScoreWithoutMods, score.TotalScore);\n}","handlingStrategy":"validation","validationCode":"var playable = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);\nif (playable.HitObjects.Count == 0)\n{ /* log + skip score migration, keep legacy TotalScore */ return; }","typeGuard":"static bool HasHitObjects(IBeatmap playable) => playable != null && playable.HitObjects.Count > 0;","tryCatchPattern":"try { convertFromLegacyTotalScore(score, ruleset, beatmap); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"no hit objects\"))\n{ /* mark score as non-migratable, continue batch */ }","preventionTips":["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."],"tags":["score-migration","beatmap","scoring","data-integrity"],"backgroundTag":null,"analyzedSha":"d9c73e12adff2feaae4a3e158d36fe5883faf6ca","analyzedAt":"2026-08-13T14:12:54.015Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}