{"record":{"id":"601f697ea61c46c2","repo":"ppy/osu","slug":"total-score-conversion-operation-returned-invalid","errorCode":null,"errorMessage":"Total score conversion operation returned invalid total of {convertedTotalScoreWithoutMods}","messagePattern":"Total score conversion operation returned invalid total of (.+?)","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"osu.Game/Database/StandardisedScoreMigrationTools.cs","lineNumber":347,"sourceCode":"                    convertedTotalScoreWithoutMods = (long)Math.Round(\r\n                        comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss))\r\n                        + dropletsPortion * dropletsHit\r\n                        + bonusProportion);\r\n                    break;\r\n\r\n                case 3:\r\n                    convertedTotalScoreWithoutMods = (long)Math.Round(\r\n                        150000 * comboProportion\r\n                        + 850000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)\r\n                        + bonusProportion);\r\n                    break;\r\n\r\n                default:\r\n                    return (score.TotalScoreWithoutMods, score.TotalScore);\r\n            }\r\n\r\n            if (convertedTotalScoreWithoutMods < 0)\r\n                throw new InvalidOperationException($\"Total score conversion operation returned invalid total of {convertedTotalScoreWithoutMods}\");\r\n\r\n            long convertedTotalScore = (long)Math.Round(convertedTotalScoreWithoutMods * modMultiplier);\r\n            return (convertedTotalScoreWithoutMods, convertedTotalScore);\r\n        }\r\n\r\n        /// <summary>\r\n        /// <para>\r\n        /// For catch, the general method of calculating the combo proportion used for other rulesets is generally useless.\r\n        /// This is because in stable score V1, catch has quadratic score progression,\r\n        /// while in stable score V2, score progression is logarithmic up to 200 combo and then linear.\r\n        /// </para>\r\n        /// <para>\r\n        /// This means that applying the naive rescale method to scores with lots of short combos (think 10x 100-long combos on a 1000-object map)\r\n        /// by linearly rescaling the combo portion as given by score V1 leads to horribly underestimating it.\r\n        /// Therefore this method attempts to counteract this by calculating the best case estimate for the combo proportion that takes all of the above into account.\r\n        /// </para>\r\n        /// <para>\r\n        /// The general idea is that aside from the <paramref name=\"scoreMaxCombo\"/> which the player is known to have hit,\r","sourceCodeStart":329,"sourceCodeEnd":365,"githubUrl":"https://github.com/ppy/osu/blob/d9c73e12adff2feaae4a3e158d36fe5883faf6ca/osu.Game/Database/StandardisedScoreMigrationTools.cs#L329-L365","documentation":"Thrown by the private convertFromLegacyTotalScore overload after the switch on ruleset legacy ID computes convertedTotalScoreWithoutMods. If the rounded result is negative, the math produced an invalid (negative) score and migration aborts rather than persisting a corrupt value. Sits just before the mod multiplier is applied.","triggerScenarios":"A legacy score whose comboProportion / accuracy / bonusProportion inputs produce a negative sum after rounding — e.g. score.Accuracy or MaxCombo values that are out of expected range, or an arithmetic edge case in the per-ruleset formula branches (cases 1-3).","commonSituations":"Bulk score migration over old/corrupt score data; scores with anomalous statistics (negative counts, NaN accuracy); a newly added ruleset formula branch with a sign error exposed by real data.","solutions":["Inspect the failing score's inputs (comboProportion, score.Accuracy, bonusProportion) for out-of-range or NaN values; treat such scores as non-migratable.","Clamp intermediate values to valid ranges (accuracy in [0,1], proportions in [0,1]) before the formula, or skip the score.","If triggered by a specific ruleset's formula, fix the branch so it cannot go negative and add unit tests covering the edge inputs."],"exampleFix":"// before\nif (convertedTotalScoreWithoutMods < 0)\n    throw new InvalidOperationException($\"Total score conversion operation returned invalid total of {convertedTotalScoreWithoutMods}\");\n\n// after (clamp + skip corrupt scores)\nconvertedTotalScoreWithoutMods = Math.Max(0, convertedTotalScoreWithoutMods);\nif (!double.IsFinite(score.Accuracy) || score.Accuracy < 0 || score.Accuracy > 1)\n{\n    LogForModel(score, \"Score accuracy out of range; skipping conversion.\");\n    return (score.TotalScoreWithoutMods, score.TotalScore);\n}","handlingStrategy":"validation","validationCode":"if (!double.IsFinite(score.Accuracy) || score.Accuracy < 0 || score.Accuracy > 1\n    || double.IsNaN(comboProportion) || double.IsNaN(bonusProportion))\n{ /* skip corrupt score, keep legacy totals */ return; }","typeGuard":"static bool IsValidScoreInput(ScoreInfo s)\n    => double.IsFinite(s.Accuracy) && s.Accuracy >= 0 && s.Accuracy <= 1\n       && s.MaxCombo >= 0;","tryCatchPattern":"try { convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"invalid total\"))\n{ /* quarantine score, log inputs for diagnosis */ }","preventionTips":["Clamp proportion inputs to [0,1] and accuracy to [0,1] before applying formulas.","Add unit tests for each ruleset's conversion branch with extreme/edge inputs.","Treat any non-finite intermediate as non-migratable."],"tags":["score-migration","scoring","data-integrity","math"],"backgroundTag":null,"analyzedSha":"d9c73e12adff2feaae4a3e158d36fe5883faf6ca","analyzedAt":"2026-08-13T14:12:54.015Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}