CSP (Custom Shaders Patch) Lua API reference for Assetto Corsa modding. Use when working with ac., ui., render., physics. APIs or any CSP Lua code.
Use this skill for Assetto Corsa Custom Shaders Patch Lua work. Prefer local bundled references first; they match the installed SDK snapshot better than memory or web snippets.
AGENTS.md or CLAUDE.md..\.claude\skills\csp-lua\scripts\find-api.ps1 "ac.getCar"
.\.claude\skills\csp-lua\scripts\find-api.ps1 "function ui.slider"
rg -n "StateCar|splinePosition|trackLengthM" .\.claude\skills\csp-lua\reference\lib.lua
reference/sdk/common/ac_state.lua: car/sim/session state.reference/sdk/common/ac_ui.lua: ui.*, windows, settings, controls.reference/sdk/common/ac_storage.lua and .d.lua: persistent storage.reference/sdk/common/io.lua and .d.lua: CSP file APIs.reference/sdk/common/ac_render.lua: render.* drawing callbacks.reference/sdk/common/ac_physics*.lua: physics.*, AI, reset, car control.reference/sdk/common/ac_web*.lua and lib_web.lua: HTTP/web APIs.reference/sdk/common/ac_audio*.lua: audio events.reference/lib.lua: generated complete EmmyLua API, enums, classes, overloads..\.claude\skills\csp-lua\scripts\logs.ps1 -l 80 -only ERROR
.\.claude\skills\csp-lua\scripts\logs.ps1 -l 120
io.popen, os.execute, start, cmd, PowerShell, and external CLI calls from Lua. Use CSP APIs such as io.open, io.save, io.load, io.scanDir, io.createDir, web.*, ac.*, and physics.*.reference/lib.lua or reference/sdk/** before using unfamiliar CSP APIs.script.update(dt) and UI draw callbacks. Expensive parsing, scans, and bulk serialization should be throttled, cached, or moved out of per-frame paths.script.* callbacks, not legacy global callbacks, unless maintaining old code. CSP looks up and caches callback functions.Lua apps live in assettocorsa/apps/lua/<app-id>/ with a manifest.ini and <app-id>.lua entry file. The official CSP Lua app wiki describes app folders, manifest.ini, lazy loading, window callbacks, UI callbacks, render callbacks, and script.update(dt).
Typical manifest window:
[ABOUT]
NAME = My App
AUTHOR = ...
VERSION = 1.0
DESCRIPTION = ...
[CORE]
LAZY = PARTIAL
[WINDOW_MAIN]
ID = main
NAME = My App
FUNCTION_MAIN = windowMain
FUNCTION_SETTINGS = windowSettings
SIZE = 400, 240
FLAGS = SETTINGS
Typical entry file:
local state = { clicks = 0 }
function script.update(dt)
local car = ac.getCar(0)
if not car then return end
end
function script.windowMain(dt)
if ui.button('Click') then
state.clicks = state.clicks + 1
end
ui.text('Clicks: ' .. state.clicks)
end
script.update(dt) runs every frame even when app windows are closed, depending on app laziness. Window functions run only when their windows are visible. UI_CALLBACKS draw outside app windows and need an explicit ui.transparentWindow() or ui.toolWindow().
Use ac.getSim() for global simulation state:
sim.dt: simulation delta in seconds; can be 0 while paused.sim.time: total AC time in milliseconds.sim.gameTime: total AC time in seconds.sim.trackLengthM: track length in meters.sim.isPaused, sim.isOnlineRace, sim.currentSessionIndex, sim.sessionTimeLeft.sim.rainIntensity, sim.roadGrip, sim.connectedCars.Use ac.getCar(index) for car state; index 0 is the player:
car.speedKmh, car.splinePosition, car.lapCount, car.lapTimeMs, car.bestLapTimeMs.car.gas, car.brake, car.clutch, car.steer, car.gear, car.handbrake.car.isLapValid, car.resetCounter, car.lastLapCutsCount, car.collisionDepth, car.isInPitlane.car.racePosition, car.fuel, tyre/wheel fields, contact/slip fields; search StateCar for the full list.Track and car helpers:
local trackId = ac.getTrackID() -- preferred spelling
local carId = ac.getCarID(0)
local carName = ac.getCarName(0, true)
local meters = ac.getSim().trackLengthM or 0
local world = ac.trackProgressToWorldCoordinate(0.25)
local progress = ac.worldCoordinateToTrackProgress(world)
local sectorName = ac.getTrackSectorName(0.25)
ac.getSim().trackId is not a valid substitute for ac.getTrackID().
Use CSP io.* APIs in runtime code:
io.createDir(path)
local f = io.open(path, 'w')
if f then
f:write(data)
f:close()
end
local data = io.load(path, '')
io.save(path, data, true) -- ensure parent folders
local exists = io.exists(path)
local isFile = io.fileExists(path)
local isDir = io.dirExists(path)
local attrs = io.getAttributes(path)
Directory scan callback pattern:
io.scanDir(folder, '*.csv', function(fileName, attrs)
if not attrs.isDirectory then
-- fileName is the item name, not always a fully normalized app path.
end
end)
Prefer typed ac.storage(layout, keyPrefix) for app settings:
local settings = ac.storage({
enabled = true,
opacity = 0.85,
pos = vec2(100, 100),
color = rgbm(0.2, 0.7, 1, 1)
}, 'my-app:')
settings.enabled = false -- auto-saved after change
ac.storage('key', default) returns an ac.StoredValue with :get() and :set(value). Direct ac.storage.key = value behaves like string-only localStorage; use it only for simple string cache entries.
CSP UI is immediate-mode, based on Dear ImGui. Draw the full UI every frame from current state. Widgets return changes; update state immediately.
sameLine() offsets that only fit one text length.##id) for controls whose visible text changes.ui.checkbox for booleans.ui.slider for bounded numeric values.ui.combo and ui.selectable for modes.ui.radioButton for 2-4 mutually exclusive choices that benefit from being visible.ui.colorPicker or ui.colorButton for colors.ui.inputText for short strings or filters.ui.iconButton with ui.Icons.* for compact tool actions.ac.ControlButton(...):control(size) for user-bindable hotkeys.ui.button for commands: load, save, reset, import, clear, open, close.Use ui.addSettings() for app settings available from the CSP taskbar/settings UI. Include stable id, icon, and size. Use padding if the default spacing feels cramped.
local openSettings = ui.addSettings({
name = 'AC Tracer Settings',
id = 'ac-tracer-settings',
icon = ui.Icons.Settings,
size = {
default = vec2(460, 520),
min = vec2(380, 360)
},
padding = vec2(14, 12)
}, function()
drawSettings()
end)
The returned function can be called with commands:
openSettings('open')
openSettings('toggle')
local isOpen = openSettings('opened')
Use ui.header() and ui.separator() to structure settings. For many settings, use tabs:
ui.tabBar('settings-tabs', function()
ui.tabItem('Display', function()
drawDisplaySettings()
end)
ui.tabItem('Telemetry', function()
drawTelemetrySettings()
end)
ui.tabItem('Hotkeys', function()
drawHotkeySettings()
end)
end)
Use collapsible groups (ui.treeNode) for advanced or experimental options:
ui.treeNode('Advanced', function()
drawAdvancedSettings()
end)
Prefer a small local row helper for settings screens. It keeps labels, controls, and margins consistent.
local LABEL_W = 170
local CONTROL_W = 220
local ROW_GAP = 6
local function formRow(label, drawControl, tooltip)
ui.alignTextToFramePadding()
ui.text(label)
if tooltip and ui.itemHovered() then ui.setTooltip(tooltip) end
ui.sameLine(LABEL_W)
ui.setNextItemWidth(CONTROL_W)
local changed, value = drawControl()
ui.offsetCursorY(ROW_GAP)
return changed, value
end
formRow('Trace length', function()
local value, changed = ui.slider('##traceLength', settings.traceLength, 5, 30, '%.0f s', true)
if changed then settings.traceLength = value end
return changed, value
end)
formRow('Comparison mode', function()
local index, changed = ui.combo('##comparisonMode', settings.modeIndex, 0, {
'Best lap',
'Previous lap',
'Reference lap'
})
if changed then settings.modeIndex = index end
return changed, index
end)
For right-aligned controls in narrow panels, compute from available width:
local controlWidth = 180
ui.text('Opacity')
ui.sameLine(math.max(120, ui.availableSpaceX() - controlWidth))
ui.setNextItemWidth(controlWidth)
settings.opacity = ui.slider('##opacity', settings.opacity, 0, 1, '%.2f')
Use ui.columns() sparingly for table-like data. Always restore to one column:
ui.columns(3, false, 'lap-columns')
ui.text('Lap'); ui.nextColumn()
ui.text('Time'); ui.nextColumn()
ui.text('Delta'); ui.nextColumn()
ui.separator()
-- rows...
ui.columns(1)
Text input:
settings.profileName = ui.inputText('##profileName', settings.profileName, ui.InputTextFlags.None, vec2(-1, 0))
Combo from a string list:
local choices = { 'Off', 'Subtle', 'Normal', 'Aggressive' }
local newIndex, changed = ui.combo('##beepMode', settings.beepModeIndex, 0, choices)
if changed then settings.beepModeIndex = newIndex end
Combo with custom selectable rows:
ui.combo('##referenceLap', selectedName, function()
for i, lap in ipairs(laps) do
if ui.selectable(lap.label, i == selectedIndex) then
selectedIndex = i
end
end
end)
Color controls:
if ui.colorButton('##traceColorPreview', settings.traceColor, ui.ColorPickerFlags.None, vec2(26, 18)) then
showColorPicker = not showColorPicker
end
if showColorPicker then
settings.traceColor = ui.colorPicker('##traceColor', settings.traceColor)
end
Icon button with tooltip:
if ui.iconButton(ui.Icons.Save, vec2(28, 28)) then
saveConfig()
end
if ui.itemHovered() then ui.setTooltip('Save settings') end
Progress and status:
ui.progressBar(importProgress, vec2(-1, 0), string.format('Importing %.0f%%', importProgress * 100))
Context menu on the previous item:
ui.text('Reference lap')
ui.itemPopup('reference-menu', function()
if ui.selectable('Clear reference') then clearReference() end
if ui.selectable('Reveal file') then revealReference() end
end)
local newOpacity, changed = ui.slider('Opacity', settings.opacity, 0, 1, '%.2f')
if changed then settings.opacity = newOpacity end
if ui.checkbox('Enabled', settings.enabled) then
settings.enabled = not settings.enabled
end
Prefer wrapper windows for app UI. Use opaque tool windows for normal tools and transparent windows for HUD overlays:
function script.windowMain(dt)
ui.toolWindow('my-app/main', vec2(100, 100), vec2(420, 280), false, true, function()
ui.text('Main content')
end)
end
function script.windowHUD(dt)
ui.transparentWindow('my-app/hud', vec2(0, 0), ui.windowSize(), true, false, function()
ui.textColored('HUD', rgbm.colors.white)
end)
end
pushFont, pushStyleVar, pushStyleColor, beginGroup, beginChild, and path operation with the correct pop/end/stroke/fill.ui.childWindow() or ui.beginChild() for scrollable lists and reserve footer height with negative sizes such as vec2(0, -32).vec2(-1, 0) generally means fill remaining width for widget sizes.##suffix when labels repeat.ui.measureText() and ui.availableSpace() to avoid clipping in compact HUDs.ui.alignTextToFramePadding() before labels that sit beside framed controls.availableSpaceX() can compute alignment.ui.separator() between conceptual groups, not between every row.Settings window:
local settingsWindow = ui.addSettings({
name = 'My App Settings',
id = 'my-app-settings',
icon = ui.Icons.Settings,
size = { default = vec2(420, 320), min = vec2(320, 220) }
}, function()
ui.header('General')
end)
App window management:
local acc = ac.accessAppWindow('ac-tracer/telemetry')
if acc and acc:valid() then acc:setVisible(true) end
ac.setAppWindowVisible('ac-tracer', 'telemetry', true)
Use ac.ControlButton for bindable controls:
local toggle = ac.ControlButton('my-app/toggle', {
keyboard = { key = ui.KeyIndex.T, ctrl = true },
gamepad = ac.GamepadButton.Y
})
if toggle:pressed() then
settings.enabled = not settings.enabled
end
toggle:control(vec2(160, 0))
For mouse input, check ui.itemHovered(), ui.mouseClicked(), ui.mouseDown(), ui.mouseWheel(), ui.mousePos(), and ui.windowPos() in the same frame you draw the hit target.
Use render.* only from proper render callbacks or render-safe contexts. RENDER_CALLBACKS in manifest.ini can point to script.Draw3D(dt) style functions. For 3D debug visuals, prefer:
render.debugLine(from, to, rgbm.colors.cyan)
render.debugSphere(center, 0.5, rgbm.colors.red)
render.debugText(pos, 'label', rgbm.colors.white, 1)
Use physics.* only when the app mode and session allow it. Check availability/permissions for invasive operations:
if physics.allowed() and ac.isCarResetAllowed() then
physics.setCarPosition(0, pos, dir)
end
For AI/traffic work, search physics.setAI*, ac.SpawnSet, and physics.teleportCarTo. For checkpoints, prefer ac.saveCarStateAsync() and ac.loadCarState() when you need a faithful car state snapshot.
For ribbons, gates, and other track-attached app visuals, register a transparent-track callback and set render state inside it:
render.on('main.track.transparent', function()
render.setBlendMode(render.BlendMode.AlphaBlend)
render.setCullMode(render.CullMode.None)
render.setDepthMode(render.DepthMode.ReadOnly)
drawWorldOverlay()
end)
Use a stable, module-level render.shaderedQuad parameter table with a fixed cacheKey, fixed shader/value keys, and directValuesExchange = true. Reuse its p1โp4 vectors and color values for every segment instead of allocating a shader table per draw. Keep logging out of render callbacks.
To draw a track-spanning gate:
ac.trackProgressToWorldCoordinateTo(progress, center).side:setCrossNormalized(tangent, vec3(0, -1, 0)).ac.getTrackAISplineSides(progress) to center and size the gate across the actual track width.For a thin racing-line ribbon, join downsampled consecutive world points with XZ-perpendicular strip quads. Raise them about 0.03โ0.05 m to avoid z-fighting, use DepthMode.ReadOnly so cars and track geometry occlude them, apply fog in the shader, and cull segments outside the useful distance/progress window.
For external telemetry or shared memory, use ac.readMemoryMappedFile(name, layout, persist) with a declared layout. Wrap setup in pcall; many users will not have the mapped file or companion DLL installed.
local ok, mmf = pcall(function()
return ac.readMemoryMappedFile('cphys_data', CPHYS_STRUCT, false)
end)
if ok and mmf then
-- read fields defensively
end
Never let optional MMF failure break the app. Log once, cache unavailability, and provide a normal CSP fallback.
Use CSP logging APIs:
ac.log('message')
ac.warn('warning')
ac.error('error')
ac.setMessage('Title', 'Description')
For AC Tracer, filter logs to avoid noise from other apps:
.\.claude\skills\csp-lua\scripts\logs.ps1 -l 80 -only ERROR
Get-Content "$env:USERPROFILE\Documents\Assetto Corsa\logs\custom_shaders_patch.log" -Tail 1000 |
Select-String "ERROR.*ac-tracer/"
Common CSP Lua failures:
attempt to index global 'X': missing local, missing module require, or wrong namespace.attempt to call field 'X': nonexistent CSP API or function not exported by a module.error loading module: syntax error, bad require() path, or module returning nothing unexpectedly.There is no known public CSP CLI command that starts or stops the Lua Debug app profiler directly. Treat profiling as an in-game/debug-app workflow unless a project includes its own bridge.
Useful CSP debug surfaces:
ac.debug(key, value) publishes live values and graphs in Lua Debug.ac.setLogSilent(true) keeps debug logging in Lua Debug without also writing every line to CSP logs.render.measure(key, drawFn) measures GPU/render sections that happen inside the passed draw function.ac.accessAppWindow('Lua Debug') can inspect, show, move, resize, or pin an existing app window once the app is available.ac.getAppWindows() lists available app windows, useful before trying to automate a debug window.Useful local toggles, generally requiring CSP/AC restart or Content Manager settings reload:
; assettocorsa/extension/config/gui.ini
[NEW_UI_2]
APP_LUA_DEBUG=1
APP_PYTHON_PROFILER=1
; assettocorsa/extension/config/general.ini
[DEV]
SILENT_LUA=0
KEEP_BACKGROUND_WORKERS_AROUND=0
PROFILE_LOADING=0
PROFILE_MEMORY=0
PROFILE_FREEZES_THRESHOLD=30
User overrides live under:
%USERPROFILE%\Documents\Assetto Corsa\cfg\extension\
%USERPROFILE%\Documents\Assetto Corsa\cfg\extension\state\imgui_settings.ini
For repeatable automation, build a small project-specific profiling bridge instead of trying to remote-control Lua Debug internals:
os.preciseClock() spans, frame counts, memory counters where available, and selected ac.debug() graphs.Use this bridge for app-level timings and regression comparisons. Do not claim it controls CSP's internal profiler unless the project has verified an explicit API or console command for that CSP build.
For this repo, run:
tests\run.cmd
If LuaJIT is available:
luajit tests/test_runner.lua
luajit -bl lib/core/state.lua > $null
When adding CSP API use, update tests/mock_ac.lua or web mocks if local tests/browser previews need the new function. Keep mocks plain and deterministic; they do not need to emulate the whole simulator.
Bundled:
reference/lib.lua: complete generated API definitions.reference/sdk/: source snapshot of ac-custom-shaders-patch/acc-lua-sdk.Useful upstream links:
https://github.com/ac-custom-shaders-patch/acc-lua-sdkhttps://github.com/ac-custom-shaders-patch/acc-lua-sdk/wiki/Lua-appshttps://github.com/ac-custom-shaders-patch/acc-lua-exampleshttps://github.com/ac-custom-shaders-patch/acc-lua-internalUse upstream only to refresh or compare; the bundled files are the first source for implementation inside this workspace.