ppy/osu · error · InvalidOperationException

Cannot recalculate score multiplier as TotalScoreWithoutMods

Error message

Cannot recalculate score multiplier as TotalScoreWithoutMods is missing.

What it means

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">.

Source

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

            return rank;
        }

        /// <summary>
        /// Updates <paramref name="scoreInfo"/>'s <see cref="ScoreInfo.TotalScore"/> to the newest score multipliers
        /// using <see cref="ScoreInfo.TotalScoreWithoutMods"/> and <see cref="ScoreInfo.Mods"/>.
        /// </summary>
        /// <param name="scoreInfo">The score to update.</param>
        /// <param name="beatmapDifficultyWithoutMods">The difficulty parameters of the beatmap applicable for the score, before application of mods.</param>
        /// <exception cref="InvalidOperationException"><paramref name="scoreInfo"/> does not have <see cref="ScoreInfo.TotalScoreWithoutMods"/> correctly populated.</exception>
        public static void UpdateToLatestScoreMultipliers(ScoreInfo scoreInfo, IBeatmapDifficultyInfo beatmapDifficultyWithoutMods)
        {
            // do nothing if the score multiplier is already up to date.
            if (scoreInfo.TotalScoreVersion >= 30000017)
                return;

            if (scoreInfo.TotalScoreWithoutMods == 0 && scoreInfo.TotalScore > 0)
                throw new InvalidOperationException($"Cannot recalculate score multiplier as {nameof(scoreInfo.TotalScoreWithoutMods)} is missing.");

            var ruleset = scoreInfo.Ruleset.CreateInstance();
            var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(beatmapDifficultyWithoutMods));
            double scoreMultiplier = scoreMultiplierCalculator.CalculateFor(scoreInfo.Mods);
            scoreInfo.TotalScore = (long)Math.Round(scoreInfo.TotalScoreWithoutMods * scoreMultiplier);
        }

        /// <summary>
        /// Used to populate the <paramref name="score"/> model using data parsed from its corresponding replay file.
        /// </summary>
        /// <param name="score">The score to run population from replay for.</param>
        /// <param name="files">A <see cref="RealmFileStore"/> instance to use for fetching replay.</param>
        /// <param name="populationFunc">
        /// Delegate describing the population to execute.
        /// The delegate's argument is a <see cref="SerializationReader"/> instance which permits to read data from the replay stream.
        /// </param>
        public static void PopulateFromReplay(this ScoreInfo score, RealmFileStore files, Action<SerializationReader> populationFunc)
        {

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Run convertFromLegacyTotalScore (the legacy score migration) first so TotalScoreWithoutMods is populated, then call UpdateToLatestScoreMultipliers.
  2. Guard before calling: if scoreInfo.TotalScoreWithoutMods == 0 && scoreInfo.TotalScore > 0, skip or backfill the baseline rather than letting it throw.
  3. For non-legacy scores with a known mod set, recompute TotalScoreWithoutMods from the score processor if available.

Example fix

// before
StandardisedScoreMigrationTools.UpdateToLatestScoreMultipliers(score, difficulty);

// after
if (score.TotalScore > 0 && score.TotalScoreWithoutMods == 0)
{
    // ensure legacy conversion ran first; otherwise skip
    Logger.Log($"Score {score.ID} missing TotalScoreWithoutMods; skipping multiplier update.");
    return;
}
StandardisedScoreMigrationTools.UpdateToLatestScoreMultipliers(score, difficulty);
Defensive patterns

Strategy: validation

Validate before calling

if (score.TotalScore > 0 && score.TotalScoreWithoutMods == 0)
{ /* backfill via legacy conversion first, or skip */ return; }
StandardisedScoreMigrationTools.UpdateToLatestScoreMultipliers(score, difficulty);

Type guard

static bool HasWithoutModsBaseline(ScoreInfo s)
    => !(s.TotalScore > 0 && s.TotalScoreWithoutMods == 0);

Try / catch

try { StandardisedScoreMigrationTools.UpdateToLatestScoreMultipliers(score, difficulty); }
catch (InvalidOperationException ex) when (ex.Message.Contains("TotalScoreWithoutMods is missing"))
{ /* queue score for baseline backfill, retry later */ }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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