ppy/osu · error · InvalidOperationException

Attempting to save a skin which is not yet tracked. Call {na

Error message

Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first.

What it means

Thrown by SkinManager.Save(skin) when skin.SkinInfo.IsManaged is false, meaning the Skin has not been attached to the realm store. Saving requires the SkinInfo to be a tracked realm entity so changes can be persisted; an untracked skin cannot be serialized back.

Source

Thrown at osu.Game/Skinning/SkinManager.cs:293

                    // save once to ensure the required json content is populated.
                    // currently this only happens on save.
                    result.PerformRead(skin => Save(skin.CreateInstance(this)));
                    CurrentSkinInfo.Value = result;
                    return true;
                }

                return false;
            });
        }

        /// <summary>
        /// Save a skin, serialising any changes to skin layouts to relevant JSON structures.
        /// </summary>
        /// <returns>Whether any change actually occurred.</returns>
        public bool Save(Skin skin)
        {
            if (!skin.SkinInfo.IsManaged)
                throw new InvalidOperationException($"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first.");

            return skinImporter.Save(skin);
        }

        /// <summary>
        /// Perform a lookup query on available <see cref="SkinInfo"/>s.
        /// </summary>
        /// <param name="query">The query.</param>
        /// <returns>The first result for the provided query, or null if no results were found.</returns>
        public Live<SkinInfo> Query(Expression<Func<SkinInfo, bool>> query)
        {
            return Realm.Run(r => r.All<SkinInfo>().FirstOrDefault(query)?.ToLive(Realm));
        }

        public event Action SourceChanged;

        public Drawable GetDrawableComponent(ISkinComponentLookup lookup) => lookupWithFallback(s => s.GetDrawableComponent(lookup));

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Call skinManager.EnsureMutableSkin() before Save() to guarantee the skin is tracked and mutable.
  2. If operating on the current skin, ensure the user has selected a non-default (mutable) skin.
  3. Wrap Save in the same flow that promotes a live skin to a tracked SkinInfo.

Example fix

// before
skinManager.Save(skin);

// after
skinManager.EnsureMutableSkin(() =>
{
    skinManager.Save(skin);
});
Defensive patterns

Strategy: validation

Validate before calling

if (!skin.SkinInfo.IsManaged)
    throw new InvalidOperationException("Call EnsureMutableSkin before Save.");

Type guard

static bool IsSkinTracked(Skin s) => s.SkinInfo.IsManaged;

Try / catch

try { skinManager.Save(skin); }
catch (InvalidOperationException) { skinManager.EnsureMutableSkin(() => skinManager.Save(skin)); }

Prevention

When it happens

Trigger: Calling Save() on a Skin that was constructed in-memory (e.g. default skin or a newly created one) without first making it tracked via EnsureMutableSkin().

Common situations: Editing the default/argon skin in-place (which is read-only/untracked); creating a Skin programmatically and trying to save before registering it; forgetting to call EnsureMutableSkin in an editor flow.

Related errors


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