{"record":{"id":"d25363c8e1b75124","repo":"ppy/osu","slug":"legacy-replay-cannot-be-converted-for-the-ruleset","errorCode":null,"errorMessage":"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}","messagePattern":"Legacy replay cannot be converted for the ruleset: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs","lineNumber":358,"sourceCode":"\r\n            foreach (var legacyFrame in legacyFrames)\r\n            {\r\n                // never allow backwards time traversal in relation to the current frame.\r\n                // this handles frames with negative delta.\r\n                // this doesn't match stable 100% as stable will do something similar to adding an interpolated \"intermediate frame\"\r\n                // at the point wherein time flow changes from backwards to forwards, but it'll do for now.\r\n                if (currentFrame != null && legacyFrame.Time < currentFrame.Time)\r\n                    continue;\r\n\r\n                replay.Frames.Add(currentFrame = convertFrame(legacyFrame, currentFrame));\r\n            }\r\n        }\r\n\r\n        private ReplayFrame convertFrame(LegacyReplayFrame currentFrame, ReplayFrame lastFrame)\r\n        {\r\n            var convertible = currentRuleset.CreateConvertibleReplayFrame();\r\n            if (convertible == null)\r\n                throw new InvalidOperationException($\"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}\");\r\n\r\n            convertible.FromLegacy(currentFrame, currentBeatmap, lastFrame);\r\n\r\n            var frame = (ReplayFrame)convertible;\r\n            frame.Time = currentFrame.Time;\r\n\r\n            return frame;\r\n        }\r\n\r\n        /// <summary>\r\n        /// Retrieves the <see cref=\"Ruleset\"/> for a specific id.\r\n        /// </summary>\r\n        /// <param name=\"rulesetId\">The id.</param>\r\n        /// <returns>The <see cref=\"Ruleset\"/>.</returns>\r\n        protected abstract Ruleset GetRuleset(int rulesetId);\r\n\r\n        /// <summary>\r\n        /// Retrieves the <see cref=\"WorkingBeatmap\"/> corresponding to an MD5 hash.\r","sourceCodeStart":340,"sourceCodeEnd":376,"githubUrl":"https://github.com/ppy/osu/blob/d9c73e12adff2feaae4a3e158d36fe5883faf6ca/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs#L340-L376","documentation":"Thrown in convertFrame when currentRuleset.CreateConvertibleReplayFrame() returns null, meaning the active ruleset does not implement legacy-to-modern replay frame conversion. The decoder was asked to translate legacy replay frames into the ruleset's native frame type but the ruleset offers no converter.","triggerScenarios":"A legacy replay (osu!stable format) is being decoded/converted for a ruleset whose CreateConvertibleReplayFrame() returns null. Common with non-legacy or custom rulesets that never defined a LegacyReplayFrame converter.","commonSituations":"Importing a stable replay for a ruleset that does not support legacy replays; switching a score's target ruleset at runtime to one without a converter; using a custom ruleset that forgot to override CreateConvertibleReplayFrame.","solutions":["Confirm the ruleset is one of the legacy-capable rulesets (osu, taiko, catch, mania) before attempting legacy replay conversion.","If authoring a custom ruleset, implement CreateConvertibleReplayFrame() to return a type deriving from ConvertibleReplayFrame.","Guard the decode path: skip conversion (or report 'unsupported') when currentRuleset.CreateConvertibleReplayFrame() == null instead of forcing it.","Check that the score's RulesetID matches the ruleset the replay was recorded under."],"exampleFix":"// before\nvar convertible = currentRuleset.CreateConvertibleReplayFrame();\nif (convertible == null)\n    throw new InvalidOperationException($\"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}\");\n\n// after (caller-side guard)\nif (currentRuleset.CreateConvertibleReplayFrame() == null)\n{\n    Logger.Log($\"Ruleset '{currentRuleset.Description}' does not support legacy replay conversion; skipping.\");\n    return;\n}","handlingStrategy":"validation","validationCode":"if (currentRuleset.CreateConvertibleReplayFrame() == null)\n{\n    Logger.Log($\"No legacy converter for {currentRuleset.Description}; skipping conversion.\");\n    return;\n}","typeGuard":"static bool RulesetSupportsLegacyConversion(Ruleset r)\n    => r.CreateConvertibleReplayFrame() != null;","tryCatchPattern":"try { convertFrame(legacyFrame, lastFrame); }\ncatch (InvalidOperationException) { /* ruleset cannot convert legacy replay */ }","preventionTips":["Check CreateConvertibleReplayFrame() != null before converting legacy replays.","Implement the converter in custom rulesets that must read stable replays.","Match the score's RulesetID to the replay's origin ruleset."],"tags":["replay","ruleset","legacy-conversion","score-decoder"],"backgroundTag":null,"analyzedSha":"d9c73e12adff2feaae4a3e158d36fe5883faf6ca","analyzedAt":"2026-08-13T14:12:54.015Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}