smithery.ai

csp-lua

CSP (Custom Shaders Patch) Lua API reference and workflow for Assetto Corsa modding.

First seen Apr 20, 2026

Installation

$ npx skills add https://smithery.ai

Summary

  • CSP (Custom Shaders Patch) Lua API reference and workflow for Assetto Corsa modding.
  • Use when writing, reviewing, debugging, or testing CSP Lua code, including Assetto Corsa Lua apps, ac.*, ui.*, render.*, physics.*, io.*, web.*, storage, app windows, telemetry, track/car state, memory-mapped files, LuaJIT/FFI, and AC Tracer-specific CSP integration.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Declared agents claude-code

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 21,310 B
  • docs SUMMARY.md 166 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

CSP Lua

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.

Workflow

  1. Read the repo instructions first when working inside a project, especially AGENTS.md or CLAUDE.md.
  2. Search the local API before inventing names:

``powershell .\.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 ``

  1. Prefer the smaller SDK source files when you know the namespace:

- reference/sdk/common/acstate.lua: car/sim/session state. - reference/sdk/common/acui.lua: ui., windows, settings, controls. - reference/sdk/common/acstorage.lua and .d.lua: persistent storage. - reference/sdk/common/io.lua and .d.lua: CSP file APIs. - reference/sdk/common/acrender.lua: render. drawing callbacks. - reference/sdk/common/acphysics.lua: physics., AI, reset, car control. - reference/sdk/common/acweb.lua and libweb.lua: HTTP/web APIs. - reference/sdk/common/acaudio.lua: audio events. - reference/lib.lua: generated complete EmmyLua API, enums, classes, overloads.

  1. Implement with immediate-mode CSP patterns, then run local Lua tests or syntax checks where available.
  2. If debugging in-game behavior, inspect filtered CSP logs:

``powershell .\.claude\skills\csp-lua\scripts\logs.ps1 -l 80 -only ERROR .\.claude\skills\csp-lua\scripts\logs.ps1 -l 120 ``

Non-Negotiables

  • Do not put shell execution in CSP runtime code. Avoid 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.*.
  • Do not guess API names. Search reference/lib.lua or reference/sdk/** before using unfamiliar CSP APIs.
  • Keep runtime work small in script.update(dt) and UI draw callbacks. Expensive parsing, scans, and bulk serialization should be throttled, cached, or moved out of per-frame paths.
  • Use script.* callbacks, not legacy global callbacks, unless maintaining old code. CSP looks up and caches callback functions.
  • Treat AC/CSP code as LuaJIT 2.1 with Lua 5.1 behavior plus CSP extensions, not stock Lua 5.4.

App Structure

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().

Core State APIs

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().

Files And Storage

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.

UI Patterns

CSP UI is immediate-mode, based on Dear ImGui. Draw the full UI every frame from current state. Widgets return changes; update state immediately.

UX Principles For CSP Apps

  • Design settings windows as forms, not piles of controls. Use consistent label columns, control widths, section headers, and vertical rhythm.
  • Put common controls first. Put destructive or rarely used actions at the bottom, separated from routine settings.
  • Align related controls on the same x positions. Avoid random sameLine() offsets that only fit one text length.
  • Keep labels stable and use hidden IDs (##id) for controls whose visible text changes.
  • Prefer native controls over text buttons:

- 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.

  • Reserve plain ui.button for commands: load, save, reset, import, clear, open, close.
  • Add tooltips for icon-only buttons, destructive actions, and controls whose effect is not obvious.
  • Use margins and spacing deliberately: small gaps inside dense HUDs, larger gaps between settings sections.
  • Keep HUD windows glanceable. Minimize labels, use consistent units, and prefer aligned numeric columns.
  • Do not redraw large custom charts with thousands of individual calls unless cached or throttled; immediate-mode draw calls have real cost.

Settings Windows

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)

Form Rows And Alignment

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)

Rich Controls

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)

Basic Widget Patterns

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

Layout Rules

  • Pair every pushFont, pushStyleVar, pushStyleColor, beginGroup, beginChild, and path operation with the correct pop/end/stroke/fill.
  • Use 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.
  • Use unique hidden IDs with ##suffix when labels repeat.
  • Use ui.measureText() and ui.availableSpace() to avoid clipping in compact HUDs.
  • Use ui.alignTextToFramePadding() before labels that sit beside framed controls.
  • Avoid hard-coded magic offsets when a label/control helper or availableSpaceX() can compute alignment.
  • Keep one visual density per window. A telemetry HUD can be dense; a settings window should breathe.
  • Put ui.separator() between conceptual groups, not between every row.
  • If a custom drawn panel has a border/background, leave at least 6-10 px inner padding before text.

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)

Input And Hotkeys

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.

Render And Physics

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.

Depth-tested world overlays

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:

  • Get its center with ac.trackProgressToWorldCoordinateTo(progress, center).
  • Sample a nearby progress value for the tangent, then get the across-track vector with side:setCrossNormalized(tangent, vec3(0, -1, 0)).
  • Use 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.

FFI And Memory-Mapped Files

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.

Debugging

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.
  • Window draw failure: check the first stack frame in the CSP log; repeated draw exceptions can hide later useful messages.

Profiling And Automation

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:

  • A PowerShell/Node CLI writes a command file or memory-mapped command block.
  • A CSP Lua app polls that command at a throttled cadence.
  • Instrumented code records os.preciseClock() spans, frame counts, memory counters where available, and selected ac.debug() graphs.
  • The bridge writes JSON/CSV snapshots to the app folder or Documents logs for external tools to consume.

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.

Testing

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.

Reference Sources

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-sdk
  • https://github.com/ac-custom-shaders-patch/acc-lua-sdk/wiki/Lua-apps
  • https://github.com/ac-custom-shaders-patch/acc-lua-examples
  • https://github.com/ac-custom-shaders-patch/acc-lua-internal

Use upstream only to refresh or compare; the bundled files are the first source for implementation inside this workspace.