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
- 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.
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
- 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.
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
- Beatmap contains no hit objects!
- Total score conversion operation returned invalid total of {
- Beatmap file {b.File!.Filename} could not be opened!
- Failed to import new beatmap
- Error when exporting {model.GetDisplayString()}: Multiple fi
AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13).
Data as JSON: /api/errors/28ff043d6e075711.
Report an issue: GitHub.