documentation/Lua guide
inso loads one Lua entry point per mapset. The script can react to playback, change hitobjects, create drawables, drive shaders, and build effects around the normal osu! map.
connect a script
Point to a plain Lua filename from the map's .inso file:
[General]
LuaEntryPoint: main.lua
The entry point must be a filename in the mapset folder. Paths containing /, \, or : are not accepted.
Callbacks are global functions. inso checks for them after loading the file, so a local function with the right name will not become an event handler.
your first script
This script creates a textured drawable, attaches a small animation, and places it on the overlay:
function on_init()
local pulse = Animation.new()
:scale(0, 1, 1, 1, 1.1, 1.1, Tween.CUBIC_OUT)
icon = Element.new("yuuma_toutetsu.png")
:set_shader("oily")
:set_animation(pulse)
icon_drawable = Drawable.new(icon, 0, 999999, Layer.OVERLAY)
:set_pos(256, 192)
:set_size(96, 96)
end
Objects returned by Drawable.new, Element.new, and Animation.new are userdata references, and will be deleted by Lua's garbage collector if the reference goes out of scope (e.g. if a reference is declared as local). Keep a reference when the object needs to stay alive. Setters internally return self, so you can chain multiple calls, like in the example.
Animation times are normalized from 0 to 1 by default. Use TimeDomain.MILLISECONDS or TimeDomain.MAP_MILLISECONDS when the animation should follow a duration or an absolute map time.
Tip: to use elements from the current skin as drawables, prepend skin: to the element name and omit the file extension, e.g. skin:hitcircle
events and time
The most useful callbacks are:
| callback | when it runs |
|---|---|
on_init() |
Once after the beatmap and script load, before play |
on_update(time_ms) |
Once per rendered frame |
on_fixed_update(time_ms) |
On a fixed music-time clock for deterministic simulation |
on_beat(beat) |
When playback crosses a beat |
on_timing_change(beat, bpm) |
When an uninherited timing point becomes active |
on_pause_change(paused) |
When playback pauses or resumes |
on_cursor_moved(x, y) |
When the cursor moves, with coordinates in playfield osupx space |
on_judgement(hitobject, judgement, timing_error_ms) |
When an object receives a judgement |
on_map_complete() |
When the last scoring object has been judged |
Use on_update for frame-based visual motion. Use on_fixed_update if you need simulation that should be reproducible across frame rates. Declaring on_fixed_update also moves scheduled callbacks onto the fixed clock.
The complete event list, signatures, and class reference are in the generated Lua API reference. on_cursor_moved(x, y) and get_cursor_pos() use playfield osupx coordinates. Convert screen pixels with Window.px_to_osupx() when needed.
beat-driven visuals
Beatmap helpers expose timing values so effects can follow the current map instead of using a guessed bpm:
function on_beat(beat)
if beat % 4 == 0 then
icon_drawable:set_beat_pulse(true)
end
end
function on_update(time_ms)
local pulse = Beatmap.get_beat_proximity()
icon_drawable:set_color(Color.rgba(255, 255, 255, 180 + pulse * 75))
end
Beatmap.get_beat_proximity() returns 1 on the beat and eases toward 0 before the next beat. Beatmap.get_music_time_ms(), Beatmap.get_bpm(), and Beatmap.get_beat_length_ms() are useful when the effect needs its own timing.
scheduling map-time actions
Schedule actions in music time rather than in wall-clock seconds:
function on_init()
schedule_at(5000, function()
Beatmap.set_skin_override("gn")
schedule_after(3000, function()
Beatmap.clear_skin_override()
end)
end)
end
Events scheduled during on_init persist and replay when the editor seeks backward. Events scheduled from another callback are one-shot, and you must reload the map with Ctrl+R to have them reapply correctly. schedule_event is the named-event version, while schedule_at and schedule_after take functions directly.
manipulating hitobjects
Query visible objects and use their base position when applying motion. set_pos is absolute, so reading get_base_pos prevents frame-by-frame offsets from accumulating:
function on_update(time_ms)
for _, hitobject in ipairs(Hitobject.get_visible_incl_followpoints()) do
local index = hitobject:get_index()
local x, y = hitobject:get_base_pos()
local angle = time_ms * 0.001 + index * 0.1
hitobject:set_pos(
x + math.cos(angle) * 12,
y + math.sin(angle) * 12
)
end
end
Use get_visible_incl_followpoints when the motion should include objects whose followpoint line is visible before the object enters its own approach window. Hitobjects can also change size, timing windows, visibility, slider behavior, and custom drawables. Check the Lua API reference for the full
reacting to judgements
Judgement callbacks can observe play results, while validate_judgement can replace them before they are committed:
function on_judgement(hitobject, judgement, timing_error_ms)
if hitobject then
print(hitobject:get_index(), judgement, timing_error_ms)
end
end
function validate_judgement(hitobject, judgement, timing_error_ms)
if hitobject and hitobject:has_any_bits(0x01) then
return Judgement.IGNORED_HIT, timing_error_ms
end
end
Return nothing from validate_judgement to keep the original result. It cannot cancel a judgement with nil; use Judgement.IGNORED_HIT when an object should not affect normal scoring.
layers and post passes
Drawables use the built-in render layers. Scripts can declare custom layers positioned relative to one of them:
function on_init()
fx_layer = Beatmap.add_layer("map_fx", {
anchor = Layer.HITOBJECTS,
above = true
})
fx = Element.new("effect.png")
fx_drawable = Drawable.new(fx, 0, 999999, fx_layer)
end
Declare custom layers in on_init. Beatmap.add_layer returns a handle that can be passed anywhere a layer is accepted, like Drawable.new, Beatmap.capture_layers, and Beatmap.add_post_pass.
Post-processing uses named render targets from the .inso file:
function on_init()
Beatmap.capture_layers("scene", {
Layer.BACKGROUND,
Layer.FOREGROUND,
Layer.HITOBJECTS,
Layer.UI,
Layer.CURSOR
})
Beatmap.add_post_pass{
shader = "crt",
src = "scene",
dst = "screen",
after = Layer.TOP
}
end
See the Shader integration guide for the matching GLSL interfaces, backbuffer setup, and render-target examples.
split scripts into files
load_file loads another Lua file from the mapset folder and returns its result:
local random = load_file("rand.lua")
function on_init()
print(random.next())
end
This is useful for keeping reusable helpers out of the entry point. The embedded runtime opens the base, table, string, and math libraries. io, os, package, and debug are not available to map scripts.
runtime notes
- Scripts must be plain source files. Precompiled Lua bytecode is rejected.
- Each protected callback has a 1.000.000 instruction count limiter to stop infinite loops from hanging the game.
- Keep expensive work out of
on_updatewhen a scheduled event or fixed update is enough. - Object declarations in events or callbacks other than
on_initmay lead to error spam when moving the playhead backwards.
The Lua API reference is generated from the engine's registrations and is an exhaustive list of available classes, methods, enums, and callback signatures.