ppy/osu · error · InvalidOperationException

Setting {nameof(CurrentSkin)}'s value directly is not suppor

Error message

Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead.

What it means

Thrown by the CurrentSkin.ValueChanged handler when a new Skin is assigned to CurrentSkin whose SkinInfo does not equal CurrentSkinInfo.Value. The API contract is that skin switching must go through CurrentSkinInfo (the source of truth), not CurrentSkin (the derived live instance). Directly setting CurrentSkin bypasses reload logic.

Source

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

            realm.Write(r =>
            {
                foreach (var skin in defaultSkins)
                {
                    if (r.Find<SkinInfo>(skin.SkinInfo.ID) == null)
                        r.Add(skin.SkinInfo.Value);
                }
            });

            CurrentSkinInfo.ValueChanged += skin =>
            {
                CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
            };

            CurrentSkin.Value = argonSkin;
            CurrentSkin.ValueChanged += skin =>
            {
                if (!skin.NewValue.SkinInfo.Equals(CurrentSkinInfo.Value))
                    throw new InvalidOperationException($"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead.");

                SourceChanged?.Invoke();
            };

            skinExporter = new LegacySkinExporter(storage)
            {
                PostNotification = obj => PostNotification?.Invoke(obj)
            };
        }

        /// <summary>
        /// Returns the dropdown ordering for use mainly by the skin selection UI.
        /// Inserts the defaults first, then 'random skin', then custom ones.
        /// Returns a list of <see cref="Live{SkinInfo}"/> items.
        /// </summary>
        public IList<Live<SkinInfo>> GetAllUsableSkins()
        {
            var skins = new List<Live<SkinInfo>>();

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Switch skins via CurrentSkinInfo.Value = skinInfo instead of mutating CurrentSkin.
  2. Read CurrentSkin only for display; never assign to it.
  3. If you must mutate a live skin, ensure it belongs to the current CurrentSkinInfo first.

Example fix

// before
skinManager.CurrentSkin.Value = mySkin;

// after
skinManager.CurrentSkinInfo.Value = mySkin.SkinInfo;
Defensive patterns

Strategy: validation

Validate before calling

// Never assign CurrentSkin directly; always go through CurrentSkinInfo.
skinManager.CurrentSkinInfo.Value = newSkinInfo;

Prevention

When it happens

Trigger: Code sets skinManager.CurrentSkin.Value = someSkin directly, bypassing CurrentSkinInfo. The change handler detects the mismatch with CurrentSkinInfo and rejects it.

Common situations: Porting skin-switching code from a system that manipulated a live Skin value; copy-paste from another Bindable pattern; an extension/component that grabbed CurrentSkin and tried to drive it.

Related errors


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