{"record":{"id":"28ff043d6e075711","repo":"ppy/osu","slug":"cannot-recalculate-score-multiplier-as-totalscorew","errorCode":null,"errorMessage":"Cannot recalculate score multiplier as TotalScoreWithoutMods is missing.","messagePattern":"Cannot recalculate score multiplier as TotalScoreWithoutMods is missing\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"osu.Game/Database/StandardisedScoreMigrationTools.cs","lineNumber":484,"sourceCode":"\r\n            return rank;\r\n        }\r\n\r\n        /// <summary>\r\n        /// Updates <paramref name=\"scoreInfo\"/>'s <see cref=\"ScoreInfo.TotalScore\"/> to the newest score multipliers\r\n        /// using <see cref=\"ScoreInfo.TotalScoreWithoutMods\"/> and <see cref=\"ScoreInfo.Mods\"/>.\r\n        /// </summary>\r\n        /// <param name=\"scoreInfo\">The score to update.</param>\r\n        /// <param name=\"beatmapDifficultyWithoutMods\">The difficulty parameters of the beatmap applicable for the score, before application of mods.</param>\r\n        /// <exception cref=\"InvalidOperationException\"><paramref name=\"scoreInfo\"/> does not have <see cref=\"ScoreInfo.TotalScoreWithoutMods\"/> correctly populated.</exception>\r\n        public static void UpdateToLatestScoreMultipliers(ScoreInfo scoreInfo, IBeatmapDifficultyInfo beatmapDifficultyWithoutMods)\r\n        {\r\n            // do nothing if the score multiplier is already up to date.\r\n            if (scoreInfo.TotalScoreVersion >= 30000017)\r\n                return;\r\n\r\n            if (scoreInfo.TotalScoreWithoutMods == 0 && scoreInfo.TotalScore > 0)\r\n                throw new InvalidOperationException($\"Cannot recalculate score multiplier as {nameof(scoreInfo.TotalScoreWithoutMods)} is missing.\");\r\n\r\n            var ruleset = scoreInfo.Ruleset.CreateInstance();\r\n            var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(beatmapDifficultyWithoutMods));\r\n            double scoreMultiplier = scoreMultiplierCalculator.CalculateFor(scoreInfo.Mods);\r\n            scoreInfo.TotalScore = (long)Math.Round(scoreInfo.TotalScoreWithoutMods * scoreMultiplier);\r\n        }\r\n\r\n        /// <summary>\r\n        /// Used to populate the <paramref name=\"score\"/> model using data parsed from its corresponding replay file.\r\n        /// </summary>\r\n        /// <param name=\"score\">The score to run population from replay for.</param>\r\n        /// <param name=\"files\">A <see cref=\"RealmFileStore\"/> instance to use for fetching replay.</param>\r\n        /// <param name=\"populationFunc\">\r\n        /// Delegate describing the population to execute.\r\n        /// The delegate's argument is a <see cref=\"SerializationReader\"/> instance which permits to read data from the replay stream.\r\n        /// </param>\r\n        public static void PopulateFromReplay(this ScoreInfo score, RealmFileStore files, Action<SerializationReader> populationFunc)\r\n        {\r","sourceCodeStart":466,"sourceCodeEnd":502,"githubUrl":"https://github.com/ppy/osu/blob/d9c73e12adff2feaae4a3e158d36fe5883faf6ca/osu.Game/Database/StandardisedScoreMigrationTools.cs#L466-L502","documentation":"Thrown by UpdateToLatestScoreMultipliers when a score has TotalScore > 0 but TotalScoreWithoutMods == 0 — the multiplier recalculation needs the without-mods baseline and cannot proceed without it. Only scores below TotalScoreVersion 30000017 reach this point. Documented via <exception cref=\"InvalidOperationException\">.","triggerScenarios":"Calling UpdateToLatestScoreMultipliers(scoreInfo, difficulty) on a score whose TotalScoreWithoutMods was never populated (e.g. imported from legacy data without running the without-mods backfill), yet TotalScore is non-zero.","commonSituations":"Scores loaded from very old databases, replay imports, or partially-migrated datasets where the without-mods column is still zero. Running the multiplier refresh before the legacy-total migration that populates TotalScoreWithoutMods.","solutions":["Run convertFromLegacyTotalScore (the legacy score migration) first so TotalScoreWithoutMods is populated, then call UpdateToLatestScoreMultipliers.","Guard before calling: if scoreInfo.TotalScoreWithoutMods == 0 && scoreInfo.TotalScore > 0, skip or backfill the baseline rather than letting it throw.","For non-legacy scores with a known mod set, recompute TotalScoreWithoutMods from the score processor if available."],"exampleFix":"// before\nStandardisedScoreMigrationTools.UpdateToLatestScoreMultipliers(score, difficulty);\n\n// after\nif (score.TotalScore > 0 && score.TotalScoreWithoutMods == 0)\n{\n    // ensure legacy conversion ran first; otherwise skip\n    Logger.Log($\"Score {score.ID} missing TotalScoreWithoutMods; skipping multiplier update.\");\n    return;\n}\nStandardisedScoreMigrationTools.UpdateToLatestScoreMultipliers(score, difficulty);","handlingStrategy":"validation","validationCode":"if (score.TotalScore > 0 && score.TotalScoreWithoutMods == 0)\n{ /* backfill via legacy conversion first, or skip */ return; }\nStandardisedScoreMigrationTools.UpdateToLatestScoreMultipliers(score, difficulty);","typeGuard":"static bool HasWithoutModsBaseline(ScoreInfo s)\n    => !(s.TotalScore > 0 && s.TotalScoreWithoutMods == 0);","tryCatchPattern":"try { StandardisedScoreMigrationTools.UpdateToLatestScoreMultipliers(score, difficulty); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"TotalScoreWithoutMods is missing\"))\n{ /* queue score for baseline backfill, retry later */ }","preventionTips":["Order migrations: populate TotalScoreWithoutMods before running multiplier refresh.","Guard every caller with the (TotalScore>0 && WithoutMods==0) precondition.","In batch jobs, collect skipped IDs for a follow-up backfill pass."],"tags":["score-migration","scoring","data-integrity","realm"],"backgroundTag":null,"analyzedSha":"d9c73e12adff2feaae4a3e158d36fe5883faf6ca","analyzedAt":"2026-08-13T14:12:54.015Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}