Repository navigation
SCRIPTED_SKIN_API English
This document records the interfaces and core classes available in the current implementation.
The recommended approach for scripted skins is to directly inherit the Skin class, rather than implementing the IScriptedSkin interface.
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) { /* ... */ }
}-
Do not write
namespace:.csxscripts should use global namespace -
Must provide
CreateInfo(): Returns aSkinInfoobject containing metadata -
Constructor signature: Must have
(SkinInfo, IStorageResourceProvider, IResourceStore<byte[]>)constructor -
Add
[UsedImplicitly]attribute: Prevents compiler warnings about unused constructors
Scripted skins provide custom behavior by overriding methods of the Skin class.
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.
public override ISample GetSample(ISampleInfo sampleInfo)Retrieves audio samples. Can return custom samples or virtual samples.
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
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
For Mania mode, you can create a dedicated transformer to override specific behaviors.
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);
}
}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.
The transformer is only injected with (ISkin skin, IBeatmap beatmap) — not IStorageResourceProvider. Custom drawables get resources as follows:
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.
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).
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) { ... }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);
}-
Recommended:
Mania<SkinName>SkinTransformer.csx -
Compatible:
ManiaTransformer.csx
-
Mania{MainScriptStem}Transformer.csx(e.g., if main script isMySkin.csx, looks forManiaMySkinTransformer.csx) 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
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(),
};- 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
In-game display name format: [Script] {Name} {Creator}
- Only scripted skins use this template
-
Creatoris automaticallyTrimEnd()when empty, leaving no trailing spaces
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.
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
resourcesisSkinManager.if (resources is EzResourceStore ez)in the main skin constructor is always false during normal play — do not use it forEzResources/.
The loader tries compatible constructor signatures in order:
-
(SkinInfo, IStorageResourceProvider, IResourceStore<byte[]>)— recommended for main scripts (SkinInfo, IStorageResourceProvider)-
(IStorageResourceProvider)— mostly for tests / manualnew
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.
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.
New classes in your script (custom Drawable, utilities, etc.) are not auto-injected. Common patterns:
-
Pass manually via constructor:
IStorageResourceProvider(actuallySkinManager),ISkin, or yourSkinsubclass - In
GetDrawableComponent:new MySprite(this)ornew MySprite(Resources) - For skin lookup chains, use
ISkin(withGetTexture/GetAnimationextensions) instead of hard-coded paths - For
EzResources/: use[Resolved] EzResourceStorein a customDrawable(see §8.3), notresourcescast
public class MyBadge : Sprite
{
public MyBadge(ISkin skin)
{
Texture = skin.GetTexture("badge");
}
}
// In GetDrawableComponent:
return new Container
{
Children = new Drawable[] { new MyBadge(this), /* ... */ }
};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.
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.) |
From narrowest to widest lookup scope:
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)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");
}EzResources/ (note, Stage, GameTheme, Modify, etc.) is served by EzResourceStore (Get, GetNote, GetAnimation, …).
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).
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
EzHUDSpritePlusetc. (they ResolvedEzResourceStoreinternally) - Use
GlobalConfigStore.EzConfigfor NoteSetName / StageName; still load textures via Resolved drawables
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.
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.) |
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.Configurationis already in scripted-skin global usings.
| 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;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 */ });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);
}
}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.
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);-
Timing: In normal gameplay both are initialized; tests may use
GlobalConfigStore.EnsureInitialized(). -
Read-first: Scripts should usually read settings, not
Setglobal player config. -
vs
[Resolved]: Static access works in customDrawables too; useGetBindablewhen UI must track live changes. -
vs
Skin.GetConfig:GetConfigis skin asset /skin.inisemantics;GlobalConfigStoreis player game settings from the settings UI.
Use in GetConfig to return a strongly typed Bindable:
return SkinUtils.As<TValue>(new Bindable<float>(64));Access the underlying skin's GetTexture / GetDrawableComponent from a transformer:
Texture? tex = Skin.GetTexture("hit300");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).
- 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 viaIResourceStore/TextureStore/EzResourceStore. - For paths outside the script folder, use
EzResourceStore.Get/FilesforEzResources; do not callFile.ReadAllText. - See
docs/ScriptedSkin-Security.mdin the repo for details.