ppy/osu · error

Legacy replay cannot be converted for the ruleset

Error message

Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}

What it means

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.

Solutions

  1. Confirm the ruleset is one of the legacy-capable rulesets (osu, taiko, catch, mania) before attempting legacy replay conversion.
  2. If authoring a custom ruleset, implement CreateConvertibleReplayFrame() to return a type deriving from ConvertibleReplayFrame.
  3. Guard the decode path: skip conversion (or report 'unsupported') when currentRuleset.CreateConvertibleReplayFrame() == null instead of forcing it.
  4. Check that the score's RulesetID matches the ruleset the replay was recorded under.

Example fix

// before
var convertible = currentRuleset.CreateConvertibleReplayFrame();
if (convertible == null)
    throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}");

// after (caller-side guard)
if (currentRuleset.CreateConvertibleReplayFrame() == null)
{
    Logger.Log($"Ruleset '{currentRuleset.Description}' does not support legacy replay conversion; skipping.");
    return;
}
Defensive patterns

Strategy: validation

Validate before calling

if (currentRuleset.CreateConvertibleReplayFrame() == null)
{
    Logger.Log($"No legacy converter for {currentRuleset.Description}; skipping conversion.");
    return;
}

Type guard

static bool RulesetSupportsLegacyConversion(Ruleset r)
    => r.CreateConvertibleReplayFrame() != null;

Try / catch

try { convertFrame(legacyFrame, lastFrame); }
catch (InvalidOperationException) { /* ruleset cannot convert legacy replay */ }

Prevention

When it happens

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

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

Related errors


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

Appendix: source

Thrown at osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs:358

            foreach (var legacyFrame in legacyFrames)
            {
                // never allow backwards time traversal in relation to the current frame.
                // this handles frames with negative delta.
                // this doesn't match stable 100% as stable will do something similar to adding an interpolated "intermediate frame"
                // at the point wherein time flow changes from backwards to forwards, but it'll do for now.
                if (currentFrame != null && legacyFrame.Time < currentFrame.Time)
                    continue;

                replay.Frames.Add(currentFrame = convertFrame(legacyFrame, currentFrame));
            }
        }

        private ReplayFrame convertFrame(LegacyReplayFrame currentFrame, ReplayFrame lastFrame)
        {
            var convertible = currentRuleset.CreateConvertibleReplayFrame();
            if (convertible == null)
                throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}");

            convertible.FromLegacy(currentFrame, currentBeatmap, lastFrame);

            var frame = (ReplayFrame)convertible;
            frame.Time = currentFrame.Time;

            return frame;
        }

        /// <summary>
        /// Retrieves the <see cref="Ruleset"/> for a specific id.
        /// </summary>
        /// <param name="rulesetId">The id.</param>
        /// <returns>The <see cref="Ruleset"/>.</returns>
        protected abstract Ruleset GetRuleset(int rulesetId);

        /// <summary>
        /// Retrieves the <see cref="WorkingBeatmap"/> corresponding to an MD5 hash.

View on GitHub (pinned to d9c73e12ad)