Skip to content

SCRIPTED_SKIN_API English

SK-la edited this page Oct 11, 2026 · 7 revisions

Scripted Skin API Documentation (English)

中文 | English

This document records the interfaces and core classes available in the current implementation.

1. Core Concept: Directly Inherit Skin Class

The recommended approach for scripted skins is to directly inherit the Skin class, rather than implementing the IScriptedSkin interface.

Basic Structure

public class MyScriptedSkin : Skin
{
    // Must provide CreateInfo() static method
    public static SkinInfo CreateInfo() => new SkinInfo
    {
        Name = "MySkin",
        Creator = "Author Name",
        Protected = false,
        InstantiationInfo = typeof(MyScriptedSkin).GetInvariantInstantiationInfo(),
    };

    protected readonly IStorageResourceProvider Resources;

    // Default constructor (for manual creation)
    public MyScriptedSkin(IStorageResourceProvider resources)
        : this(CreateInfo(), resources)
    {
    }

    // Constructor used by script loader
    [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
    public MyScriptedSkin(SkinInfo skin, IStorageResourceProvider resources, IResourceStore<byte[]>? fallbackStore = null)
        : base(skin, resources, fallbackStore)
    {
        Resources = resources;
    }

    // Override Skin methods
    public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) 
        => Textures?.Get(componentName, wrapModeS, wrapModeT);

    public override ISample GetSample(ISampleInfo sampleInfo) { /* ... */ }
    public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { /* ... */ }
    public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup) { /* ... */ }
}

Key Points

  • Do not write namespace: .csx scripts should use global namespace
  • Must provide CreateInfo(): Returns a SkinInfo object containing metadata
  • Constructor signature: Must have (SkinInfo, IStorageResourceProvider, IResourceStore<byte[]>) constructor
  • Add [UsedImplicitly] attribute: Prevents compiler warnings about unused constructors

2. Skin Base Class Core Methods

Scripted skins provide custom behavior by overriding methods of the Skin class.

GetTexture

public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)

Retrieves textures from texture storage. Typically delegates directly to the base class's Textures property.

GetSample

public override ISample GetSample(ISampleInfo sampleInfo)

Retrieves audio samples. Can return custom samples or virtual samples.

GetDrawableComponent

public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)

Retrieves drawable components. This is the core method for scripted skins, used to customize HUD elements, game interface, etc.

Supported lookup types:

  • GlobalSkinnableContainerLookup: Global containers (e.g., MainHUDComponents)
  • ManiaSkinComponentLookup: Mania mode specific components
  • Other ruleset-specific component lookups

GetConfig

public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)

Retrieves configuration values. Supports:

  • GlobalSkinColours: Global color configuration
  • SkinComboColourLookup: Combo colors
  • ManiaSkinConfigurationLookup: Mania mode configuration
  • Other skin configuration items

3. Mania Transformer

For Mania mode, you can create a dedicated transformer to override specific behaviors.

Basic Structure

public class ManiaMySkinTransformer : SkinTransformer
{
    private readonly ManiaBeatmap beatmap;

    public ManiaMySkinTransformer(ISkin skin, IBeatmap beatmap)
        : base(skin)
    {
        this.beatmap = (ManiaBeatmap)beatmap;
    }

    public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
    {
        switch (lookup)
        {
            // In-game Mania skin parts (column bg, notes, stage, etc.)
            case ManiaSkinComponentLookup maniaLookup:
                switch (maniaLookup.Component)
                {
                    case ManiaSkinComponents.ColumnBackground:
                        return new ManiaCustomColumnBg(Skin);

                    case ManiaSkinComponents.Note:
                        return new ManiaCustomNote(Skin);
                }
                break;

            // Mania HUD container (only when Ruleset is Mania)
            case GlobalSkinnableContainerLookup containerLookup
                when containerLookup.Lookup == GlobalSkinnableContainers.MainHUDComponents
                     && containerLookup.Ruleset != null:
                return new DefaultSkinComponentsContainer(_ => { })
                {
                    Children = new Drawable[] { new ManiaHudBadge() },
                };
        }

        return base.GetDrawableComponent(lookup);
    }

    public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
    {
        if (lookup is ManiaSkinConfigurationLookup maniaLookup)
        {
            switch (maniaLookup.Lookup)
            {
                case LegacyManiaSkinConfigurationLookups.ColumnWidth:
                    return SkinUtils.As<TValue>(new Bindable<float>(64));
                case LegacyManiaSkinConfigurationLookups.HitPosition:
                    return SkinUtils.As<TValue>(new Bindable<float>(430));
            }
        }
        return base.GetConfig<TLookup, TValue>(lookup);
    }
}

GetDrawableComponent switch guide

The transformer receives the same lookup types as the main skin:

lookup type Typical use
ManiaSkinComponentLookup Replace column background, notes, LN, stage, hit explosions, etc.
GlobalSkinnableContainerLookup Replace/layout Mania play HUD (MainHUDComponents with Ruleset != null)
SkinComponentLookup<HitResult> Replace judgement sprites (advanced; built-in Ez components often suffice)

ManiaSkinComponents values:

ColumnBackground, HitTarget, KeyArea, Note, HoldNoteHead, HoldNoteTail, HoldNoteBody, HitExplosion, StageBackground, StageForeground, BarLine

Unhandled lookups should return base.GetDrawableComponent(lookup) to fall back to the main skin script.

Returning a custom Drawable

The transformer is only injected with (ISkin skin, IBeatmap beatmap) — not IStorageResourceProvider. Custom drawables get resources as follows:

Option A: [Resolved] (recommended when the drawable enters the scene tree)

Your class must inherit Drawable and be returned (directly or inside DefaultSkinComponentsContainer.Children). EzResourceStore is injected during Load():

using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.EzOsuGame;

public partial class ManiaHudBadge : Sprite
{
    [Resolved]
    private EzResourceStore ez { get; set; } = null!;

    [BackgroundDependencyLoader]
    private void load()
    {
        Texture = ez.Get("Modify/Tachie/badge");
        RelativeSizeAxes = Axes.None;
        Size = new Vector2(64);
    }
}

[Resolved] is only valid after Load — not in the constructor. Add using osu.Framework.Allocation explicitly.

Option B: Pass ISkin via constructor (new ... (Skin) in the transformer)

Use when you want textures resolved at construction time without [Resolved]:

public class ManiaCustomColumnBg : Box
{
    public ManiaCustomColumnBg(ISkin skin)
    {
        RelativeSizeAxes = Axes.Both;
        // Uses skin lookup chain: script folder / skin.ini / main skin GetTexture override
    }
}

public class ManiaCustomNote : Sprite
{
    public ManiaCustomNote(ISkin skin)
    {
        Texture = skin.GetTexture("note/custom/mynote");  // image in script folder via main skin GetTexture
    }
}

SkinTransformer.Skin is the underlying scripted skin (ScriptedSkinWrapper); texture lookups go through the main skin's GetTexture (typically script folder only).

Option C: EzResources/ → use [Resolved] (do not cast resources)

EzResourceStore is not in the transformer / main skin constructor's resources. For EzResources/note, Modify/, etc., return a custom Drawable with [Resolved] EzResourceStore (same as Option A), or use built-in EzHUD* components.

// Correct
return new ManiaEzResourceSprite();

public partial class ManiaEzResourceSprite : Sprite
{
    [Resolved] private EzResourceStore ez { get; set; } = null!;

    [BackgroundDependencyLoader]
    private void load() => Texture = ez.Get("Modify/Tachie/badge");
}
// Wrong — never true in scripted skin load path
if (Resources is EzResourceStore ez) { ... }

Full example: custom column background + HUD badge

public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
    switch (lookup)
    {
        case ManiaSkinComponentLookup maniaLookup:
            switch (maniaLookup.Component)
            {
                case ManiaSkinComponents.ColumnBackground:
                    return new ManiaCustomColumnBg(Skin)
                    {
                        Colour = new Color4(0.1f, 0.1f, 0.15f, 1f),
                    };
            }
            break;

        case GlobalSkinnableContainerLookup { Lookup: GlobalSkinnableContainers.MainHUDComponents, Ruleset: not null }:
            return new DefaultSkinComponentsContainer(container =>
            {
                var badge = container.ChildrenOfType<ManiaHudBadge>().FirstOrDefault();
                badge?.MoveTo(new Vector2(10, 10));
            })
            {
                Children = new Drawable[] { new ManiaHudBadge() },
            };
    }

    return base.GetDrawableComponent(lookup);
}

Naming Conventions

  • Recommended: Mania<SkinName>SkinTransformer.csx
  • Compatible: ManiaTransformer.csx

Lookup Priority

  1. Mania{MainScriptStem}Transformer.csx (e.g., if main script is MySkin.csx, looks for ManiaMySkinTransformer.csx)
  2. ManiaTransformer.csx

The transformer prioritizes handling GetConfig and GetDrawableComponent, with unhandled requests falling back to the main skin.

Full copy-paste example (EzResources BPM animation + pause/stop vs continue): Mania Transformer Example

4. SkinInfo Metadata

Provide skin metadata through the CreateInfo() static method.

public static SkinInfo CreateInfo() => new SkinInfo
{
    Name = "MySkin",           // Skin display name
    Creator = "Author Name",   // Author name (can be empty)
    Protected = false,         // Whether protected (editing creates a copy when true)
    InstantiationInfo = typeof(MySkin).GetInvariantInstantiationInfo(),
};

Field Descriptions

  • Name: Display name of the skin in the list
  • Creator: Author name, automatically omitted from display name when empty
  • Protected: When set to true, editor modifications first create a copy without directly modifying the original skin
  • InstantiationInfo: Type instantiation information used for reflection-based instance creation

Display Name Format

In-game display name format: [Script] {Name} {Creator}

  • Only scripted skins use this template
  • Creator is automatically TrimEnd() when empty, leaving no trailing spaces

5. Constructor Injection (DI) Model

Scripted skins do not use a standalone DI container (unlike ASP.NET auto-wiring). SandboxedScriptRunner compiles the script, then creates instances by matching constructor parameters.

What the main skin script receives

The game loads the primary .csx via:

LoadScriptAsync<IScriptedSkin>(scriptPath, skinInfo, resources)
Parameter Type Source Notes
skinInfo SkinInfo Skin list metadata Name, Creator, Protected, etc.
resources IStorageResourceProvider SkinManager Renderer, audio, Realm, files/ (not EzResourceStore)
fallbackStore (auto-appended) IResourceStore<byte[]> Script directory NativeStorage Images, skin.ini, layout JSON next to .csx

Note: At runtime resources is SkinManager. if (resources is EzResourceStore ez) in the main skin constructor is always false during normal play — do not use it for EzResources/.

The loader tries compatible constructor signatures in order:

  1. (SkinInfo, IStorageResourceProvider, IResourceStore<byte[]>) — recommended for main scripts
  2. (SkinInfo, IStorageResourceProvider)
  3. (IStorageResourceProvider) — mostly for tests / manual new

fallbackStore is injected automatically and maps to EzResources/ScriptedSkin/<FolderName>/. Pass it to base(skin, resources, fallbackStore) so the Skin base class merges it into the internal resource chain.

What the Mania Transformer receives

LoadScriptAsync<SkinTransformer>(transformerPath, sourceSkin, beatmap)
Parameter Type Notes
sourceSkin ISkin Current scripted skin (ScriptedSkinWrapper); use Skin property for texture/HUD lookups
beatmap IBeatmap Current beatmap; cast to ManiaBeatmap in Mania

Transformers do not receive a separate IStorageResourceProvider. Use Skin.GetTexture(...) or pass a Resources field saved on the main skin.

Passing dependencies to custom helper classes

New classes in your script (custom Drawable, utilities, etc.) are not auto-injected. Common patterns:

  1. Pass manually via constructor: IStorageResourceProvider (actually SkinManager), ISkin, or your Skin subclass
  2. In GetDrawableComponent: new MySprite(this) or new MySprite(Resources)
  3. For skin lookup chains, use ISkin (with GetTexture / GetAnimation extensions) instead of hard-coded paths
  4. For EzResources/: use [Resolved] EzResourceStore in a custom Drawable (see §8.3), not resources cast
public class MyBadge : Sprite
{
    public MyBadge(ISkin skin)
    {
        Texture = skin.GetTexture("badge");
    }
}

// In GetDrawableComponent:
return new Container
{
    Children = new Drawable[] { new MyBadge(this), /* ... */ }
};

6. IStorageResourceProvider (game-injected interface)

The resources argument implements this interface. Scripted skins receive SkinManager (SkinManager also implements ISkinSource and IStorageResourceProvider).

EzResourceStore is a separate instance in the game DI cache for HUD components via [Resolved] — it is not passed as resources to the main skin script.

Member Type Purpose (under SkinManager)
Renderer IRenderer Required when creating TextureStore and drawables
AudioManager AudioManager? Sample store and playback
Files IResourceStore<byte[]> files/ user directory (not EzResources/)
Resources IResourceStore<byte[]> Built-in DLL resources
RealmAccess RealmAccess Realm database access (advanced; use with care)
CreateTextureLoaderStore(underlyingStore) IResourceStore<TextureUpload>? Wraps a byte store into a texture-loading store

Save as a field for Renderer, script-folder TextureStore, etc. For EzResources/, use [Resolved] per §8.3.


7. Accessible Skin base members

When inheriting Skin, besides overrides you can use:

Member Description
Textures TextureStore? — looks up skin.ini, Realm skin files, and fallbackStore
Samples ISampleStore? — .wav / .mp3 / .ogg in the skin directory
FallbackStore IResourceStore<byte[]>? — script folder only (same level as .csx)
Configuration SkinConfiguration — parsed skin.ini (combo colours, Mania column width, etc.)
SkinInfo Live<SkinInfo> — current skin metadata
Name Skin name string
LayoutInfos HUD layout JSON (MainHUDComponents.json, etc.)

8. Loading images and animations: which API to use

From narrowest to widest lookup scope:

8.1 Images in the script directory (next to .csx)

Place files under EzResources/ScriptedSkin/<FolderName>/, e.g. badge.png, frames-0.png.

Option A — skin lookup chain (recommended)

Override GetTexture and delegate to the base; custom drawables look up via ISkin:

public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
    => Textures?.Get(componentName, wrapModeS, wrapModeT);

// In a custom class:
Texture? tex = skin.GetTexture("badge");

Option B — use Textures directly

Texture? tex = Textures?.Get("badge");

Option C — frame animation (official extension)

On any ISkin instance (this or Skin property):

Drawable? anim = this.GetAnimation("frames", animatable: true, looping: true);
// Probes frames-0, frames-1, ... (same rules as legacy skins)

8.2 Script directory only, bypassing skin.ini

Use injected fallbackStore, or create a TextureStore yourself:

if (FallbackStore != null)
{
    var loader = Resources.CreateTextureLoaderStore(FallbackStore);
    using var localTextures = new TextureStore(Resources.Renderer, loader);
    Texture? tex = localTextures.Get("overlay");
}

8.3 Global EzResources pack

EzResources/ (note, Stage, GameTheme, Modify, etc.) is served by EzResourceStore (Get, GetNote, GetAnimation, …).

Reliable: [Resolved] in a custom Drawable

EzResourceStore is cached in game DI (OsuGameBase) — a different object from SkinManager injected into the main skin script. In drawables that enter the scene tree:

using osu.Framework.Allocation;
using osu.Game.EzOsuGame;

public partial class MyEzSprite : Sprite
{
    [Resolved]
    private EzResourceStore ez { get; set; } = null!;

    [BackgroundDependencyLoader]
    private void load()
    {
        Texture? note = ez.GetNote("whitenote");
        Texture? stage = ez.GetStage("StageLeft");
        Texture? any   = ez.Get("GameTheme/foo/bar");
        Drawable? anim = ez.GetAnimation("ColumnLight/EzColumnLight");
    }
}

You can also [Resolved] EzLocalTextureFactory (Ez Pro note/column integration with Ez2Setting).

Unreliable: if (Resources is EzResourceStore ez) in main skin constructor

Script load calls LoadScriptAsync(..., skinInfo, skinManager) — resources is always SkinManager. Patterns like EzProSkin.csx's if (resources is EzResourceStore) are unreachable on the real scripted-skin path; do not copy for player-made skins.

Alternatives without writing Drawable:

  • Put images in the script folder and use Textures / GetTexture / FallbackStore (§8.1)
  • Use built-in EzHUDSpritePlus etc. (they Resolved EzResourceStore internally)
  • Use GlobalConfigStore.EzConfig for NoteSetName / StageName; still load textures via Resolved drawables

8.4 Showing images in a custom Drawable (full example)

public class ScriptLocalSprite : Sprite
{
    public ScriptLocalSprite(IStorageResourceProvider resources, IResourceStore<byte[]> scriptStore, string path)
    {
        var loader = resources.CreateTextureLoaderStore(scriptStore);
        using var store = new TextureStore(resources.Renderer, loader);
        Texture = store.Get(path);
    }
}

// Save scriptStore in the main skin constructor:
private readonly IResourceStore<byte[]>? scriptStore;

public MyScriptedSkin(SkinInfo skin, IStorageResourceProvider resources, IResourceStore<byte[]>? fallbackStore = null)
    : base(skin, resources, fallbackStore)
{
    Resources = resources;
    scriptStore = fallbackStore;
}

// In GetDrawableComponent:
if (scriptStore != null)
    children.Add(new ScriptLocalSprite(Resources, scriptStore, "my_icon"));

Omit file extensions in paths (osu skin convention). Files live in the script folder.


9. EzResourceStore API reference

These APIs belong to EzResourceStore. In scripted skins, call them via [Resolved] EzResourceStore inside a Drawable (§8.3). Do not use is EzResourceStore on constructor resources.

Method Description
Get(path) Load texture by EzResources-relative path
GetNote(component) Note component under current NoteSet
GetStage(component) Stage component under current Stage
GetTextures(...) / GetAnimation(...) Frame animation with - / _ separators
GetAnimationFromTemplate(...) Judgement animation via {result}, {0} templates
GetSample(name) Sample under EzResources/Samples
BuildNotePath / BuildStagePath Build path strings
GetEzResourceStream(path) Read non-image resource streams (text, config, etc.)

10. GlobalConfigStore static global configuration

GlobalConfigStore (osu.Game.EzOsuGame.Configuration) is Ez2Lazer's static global config entry, populated during OsuGameBase startup. Scripted skins and Mania transformers can read it without [Resolved] from anywhere in the script.

The class name is GlobalConfigStore (not GlobalConfig). osu.Game.EzOsuGame.Configuration is already in scripted-skin global usings.

Two config managers

Static property Type Settings
GlobalConfigStore.Config OsuConfigManager Official lazer settings (OsuSetting enum)
GlobalConfigStore.EzConfig Ez2ConfigManager Ez2Lazer extension settings (Ez2Setting enum)

Assigned at game startup (OsuGameBase):

GlobalConfigStore.Config = LocalConfig;
GlobalConfigStore.EzConfig = Ez2ConfigManager;

How to read

Both inherit framework ConfigManager<T>:

T value = GlobalConfigStore.EzConfig.Get<T>(Ez2Setting.SomeSetting);
T official = GlobalConfigStore.Config.Get<T>(OsuSetting.SomeSetting);

IBindable<T> bindable = GlobalConfigStore.EzConfig.GetBindable<T>(Ez2Setting.SomeSetting);
bindable.BindValueChanged(e => { /* react to setting changes */ });

Typical use in Mania Transformer

See docs/EzProScriptedSkin/ManiaEzProSkinTransformer.csx:

using osu.Game.EzOsuGame.Configuration;

public class ManiaMySkinTransformer : SkinTransformer
{
    private readonly Ez2ConfigManager ezConfig;
    private readonly IBindable<double> hitPosition;

    public ManiaMySkinTransformer(ISkin skin, IBeatmap beatmap)
        : base(skin)
    {
        ezConfig = GlobalConfigStore.EzConfig;
        hitPosition = ezConfig.GetBindable<double>(Ez2Setting.HitPosition);
    }
}

Common Ez2Setting examples (skin/gameplay)

Ez2Setting Meaning
ColumnWidth / SpecialFactor Lane width and special-lane multiplier
HitPosition Hit line position
NoteSetName / StageName Current note / stage resource pack
ManiaHitMode / ManiaHealthMode Mania hit / health mode
ColumnTypeOf4K … ColumnTypeOf10K Per keymode column types

Full list: Ez2Setting enum in Ez2ConfigManager.cs.

Common OsuSetting examples (official)

OsuSetting Meaning
AudioOffset Global audio offset
DimLevel / BlurLevel Background dim / blur
ShowStoryboard Storyboard visibility
GameplayCursorSize In-game cursor size

Requires using osu.Game.Configuration; for OsuSetting.

double offset = GlobalConfigStore.Config.Get<double>(OsuSetting.AudioOffset);

Notes

  • Timing: In normal gameplay both are initialized; tests may use GlobalConfigStore.EnsureInitialized().
  • Read-first: Scripts should usually read settings, not Set global player config.
  • vs [Resolved]: Static access works in custom Drawables too; use GetBindable when UI must track live changes.
  • vs Skin.GetConfig: GetConfig is skin asset / skin.ini semantics; GlobalConfigStore is player game settings from the settings UI.

11. Helper APIs

SkinUtils.As<TValue>(bindable)

Use in GetConfig to return a strongly typed Bindable:

return SkinUtils.As<TValue>(new Bindable<float>(64));

SkinTransformer.Skin

Access the underlying skin's GetTexture / GetDrawableComponent from a transformer:

Texture? tex = Skin.GetTexture("hit300");

Automatic global using

These namespaces are imported at compile time (see ScriptedSkinCompilation); you do not need explicit using for them:

System, System.Linq, osu.Framework.Graphics*, osu.Framework.IO.Stores, osu.Game.Skinning, osu.Game.EzOsuGame.HUD, osu.Game.Rulesets.Mania*, osuTK, etc.

You still need explicit using for types outside that list (e.g. osu.Game.EzOsuGame for EzResourceStore).


12. Security and limits

  • Scripts compile and run inside the game process — trusted local code, not a full sandbox.
  • Source must not contain strings like System.IO, System.Net, System.Reflection, etc. (SandboxedScriptRunner.validateScriptSafety). Access files via IResourceStore / TextureStore / EzResourceStore.
  • For paths outside the script folder, use EzResourceStore.Get / Files for EzResources; do not call File.ReadAllText.
  • See docs/ScriptedSkin-Security.md in the repo for details.

Clone this wiki locally