local settings = ac.storage({ showHidden = false, showDisclaimer = true, higherPrecision = false }) local car = ac.getCar(0) or error() local ignoreChangesUntil = 0 local onConfigChange ---@class MappedConfig ---@field filename string ---@field ini ac.INIConfig ---@field data table ---@field original table ---@field map table local MappedConfig = class('MappedConfig', function(filename, map) local ini = ac.INIConfig.load(filename) local data = ini:mapConfig(map) local key = 'app.ControllerTweaks:'..filename -- local original = stringify.tryParse(ac.load(key)) local original = nil -- TODO: REMOVE THIS LINE if not original then ac.store(key, stringify(data)) original = stringify.parse(stringify(data)) end return {filename = filename, ini = ini, map = map, data = data, original = original} end, class.NoInitialize) function MappedConfig:reload() self.ini = ac.INIConfig.load(self.filename) or self.ini self.data = self.ini:mapConfig(self.map) end ---@param section string ---@param key string ---@param value number|boolean ---@param triggerControlReload boolean? function MappedConfig:set(section, key, value, triggerControlReload, hexFormat) if not self.data[section] then self.data[section] = {} end if type(value) == 'number' and not (value > -1e9 and value < 1e9) then error('Sanity check failed: '..tostring(value)) end if self.data[section][key] == value then return end self.data[section][key] = value setTimeout(function () ac.log('Saving updated value: '..tostring(value)) if onConfigChange then onConfigChange() end self.ini:setAndSave(section, key, hexFormat and string.format('0x%x', self.data[section][key]) or self.data[section][key]) if triggerControlReload ~= false then setTimeout(function () ac.log('Reloading control settings now') ac.reloadControlSettings() end, 0.02, 'reload') end ignoreChangesUntil = ui.time() + 4 end, 0.02, section..key) end -- Actual configs: local cfgControls = MappedConfig(ac.getFolder(ac.FolderID.Cfg)..'/controls.ini', { HEADER = { INPUT_METHOD = ac.INIConfig.OptionalString }, KEYBOARD = { MOUSE_STEER = false, MOUSE_ACCELERATOR_BRAKE = false, STEERING_SPEED = 1.75, STEERING_OPPOSITE_DIRECTION_SPEED = 2.5, STEER_RESET_SPEED = 1.8, MOUSE_SPEED = 0.1, GAS = 0, BRAKE = 0, RIGHT = 0, LEFT = 0, GEAR_UP = 0, GEAR_DOWN = 0, HANDBRAKE = 0 }, __EXT_KEYBOARD = { SHIFT_WITH_WHEEL = false, SHIFT_WITH_XBUTTONS = false }, __EXT_KEYBOARD_GAS_RAW = { OVERRIDE = 0, LAG_UP = 0.5, LAG_DOWN = 0.2, KEY_MODIFICATOR = -1, KEY = -1, MOUSE = 0, MOUSE_MODIFICATOR = 0 }, X360 = { STEER_GAMMA = 2, STEER_FILTER = 0.7, SPEED_SENSITIVITY = 0.1, STEER_DEADZONE = 0, RUMBLE_INTENSITY = 0.5, STEER_SPEED = 0.2, STEER_THUMB = 'LEFT', JOYPAD_INDEX = 0 }, STEER = { JOY = 0, AXLE = -1, LOCK = 1080, FF_GAIN = 0.8, FILTER_FF = 0, STEER_GAMMA = 1, SPEED_SENSITIVITY = 0, DEBOUNCING_MS = 50 }, FF_TWEAKS = { MIN_FF = 0.03, CENTER_BOOST_GAIN = 0, CENTER_BOOST_RANGE = 0.1 }, FF_ENHANCEMENT = { CURBS = 0.3, ROAD = 0.3, SLIPS = 0.15, ABS = 0.2 }, FF_ENHANCEMENT_2 = { UNDERSTEER = 0 }, FF_SKIP_STEPS = { VALUE = 1 }, BRAKES = { JOY = 0, AXLE = -1, MIN = 1, MAX = -1, GAMMA = 2.8 }, -- Gamepad buttons: ABS = { XBOXBUTTON = '' }, ABSDN = { XBOXBUTTON = '' }, ABSUP = { XBOXBUTTON = '' }, ACTION_CELEBRATE = { XBOXBUTTON = '' }, ACTION_CHANGE_CAMERA = { XBOXBUTTON = '' }, ACTION_CLAIM = { XBOXBUTTON = '' }, ACTION_HEADLIGHTS = { XBOXBUTTON = '' }, ACTION_HEADLIGHTS_FLASH = { XBOXBUTTON = '' }, ACTION_HORN = { XBOXBUTTON = '' }, ACTIVATE_AI = { XBOXBUTTON = '' }, AUTO_SHIFTER = { XBOXBUTTON = '' }, BALANCEDN = { XBOXBUTTON = '' }, BALANCEUP = { XBOXBUTTON = '' }, DRIVER_NAMES = { XBOXBUTTON = '' }, DRS = { XBOXBUTTON = '' }, ENGINE_BRAKE_DN = { XBOXBUTTON = '' }, ENGINE_BRAKE_UP = { XBOXBUTTON = '' }, FFWD = { XBOXBUTTON = '' }, GEARDN = { XBOXBUTTON = '' }, GEARUP = { XBOXBUTTON = '' }, GLANCEBACK = { XBOXBUTTON = '' }, GLANCELEFT = { XBOXBUTTON = '' }, GLANCERIGHT = { XBOXBUTTON = '' }, HANDBRAKE = { XBOXBUTTON = '' }, HIDE_APPS = { XBOXBUTTON = '' }, HIDE_DAMAGE = { XBOXBUTTON = '' }, IDEAL_LINE = { XBOXBUTTON = '' }, KERS = { XBOXBUTTON = '' }, MGUH_MODE = { XBOXBUTTON = '' }, MGUK_DELIVERY_DN = { XBOXBUTTON = '' }, MGUK_DELIVERY_UP = { XBOXBUTTON = '' }, MGUK_RECOVERY_DN = { XBOXBUTTON = '' }, MGUK_RECOVERY_UP = { XBOXBUTTON = '' }, MOUSE_STEERING = { XBOXBUTTON = '' }, NEXT_CAR = { XBOXBUTTON = '' }, NEXT_LAP = { XBOXBUTTON = '' }, PAUSE_REPLAY = { XBOXBUTTON = '' }, PLAYER_CAR = { XBOXBUTTON = '' }, PREVIOUS_CAR = { XBOXBUTTON = '' }, PREVIOUS_LAP = { XBOXBUTTON = '' }, RESET_RACE = { XBOXBUTTON = '' }, REV = { XBOXBUTTON = '' }, SHOW_DAMAGE = { XBOXBUTTON = '' }, SLOWMO = { XBOXBUTTON = '' }, STARTER = { XBOXBUTTON = '' }, START_REPLAY = { XBOXBUTTON = '' }, TCDN = { XBOXBUTTON = '' }, TCUP = { XBOXBUTTON = '' }, TRACTION_CONTROL = { XBOXBUTTON = '' }, TURBODN = { XBOXBUTTON = '' }, TURBOUP = { XBOXBUTTON = '' }, __EXT_KEYBOARD_CLUTCH = { XBOXBUTTON = '' }, __EXT_SIM_PAUSE = { XBOXBUTTON = '' }, }) local cfgFFPostProcess = MappedConfig(ac.getFolder(ac.FolderID.Cfg)..'/ff_post_process.ini', { HEADER = { TYPE = ac.INIConfig.OptionalString, ENABLED = false }, GAMMA = { VALUE = 1 }, LUT = { CURVE = ac.INIConfig.OptionalString } }) local cfgSystem = MappedConfig(ac.getFolder(ac.FolderID.Root)..'/system/cfg/assetto_corsa.ini', { FF_EXPERIMENTAL = { ENABLE_GYRO = false, DAMPER_MIN_LEVEL = 0, DAMPER_GAIN = 1 }, LOW_SPEED_FF = { SPEED_KMH = 3, MIN_VALUE = 0.01 } }) local cfgCSPGeneral = MappedConfig(ac.getFolder(ac.FolderID.ExtCfgUser)..'/general.ini', { CONTROL = { NO_MOUSE_STEERING_FOR_INACTIVE = false } }) local cfgFFBTweaks = MappedConfig(ac.getFolder(ac.FolderID.ExtCfgUser)..'/ffb_tweaks.ini', { BASIC = { ENABLED = true }, GYRO2 = { ENABLED = false, STRENGTH = 0.25 }, POSTPROCESSING = { RANGE_COMPRESSION = 1, RANGE_COMPRESSION_ASSIST = false }, }) -- Listen to changes in files: local curves = {} local curvesCache = {} local function reloadCurves() curves = io.scanDir(ac.getFolder(ac.FolderID.Cfg), '*.lut') if cfgFFPostProcess.data.LUT.CURVE and not table.indexOf(curves, cfgFFPostProcess.data.LUT.CURVE) then table.insert(curves, cfgFFPostProcess.data.LUT.CURVE) end if cfgFFPostProcess.original.LUT.CURVE and not table.indexOf(curves, cfgFFPostProcess.original.LUT.CURVE) then table.insert(curves, cfgFFPostProcess.original.LUT.CURVE) end table.sort(curves, function (a, b) return a < b end) table.clear(curvesCache) end reloadCurves() -- ac.onCSPConfigChanged(ac.CSPModuleID.FFBTweaks, function () -- ac.log('FFB tweaks module changed!') -- end) ac.onFolderChanged(ac.getFolder(ac.FolderID.ACDocuments)..'/cfg', '{ ?controls.ini | ?ff_post_process.ini | ?.lut }', false, function (files) if ui.time() < ignoreChangesUntil then return end if table.some(files, function (file) return string.lower(file):match('%.lut') end) then reloadCurves() elseif table.some(files, function (file) return file:match('controls%.ini') or file:match('ff_post_process%.ini') end) then ac.log('Reload controls settings') cfgControls:reload() cfgFFPostProcess:reload() ac.reloadControlSettings() end end) ac.onFolderChanged(ac.getFolder(ac.FolderID.Root)..'/system/config', '?assetto_corsa.ini`', false, function () if ui.time() < ignoreChangesUntil then return end ac.log('Reload system controls settings') cfgSystem:reload() ac.reloadControlSettings() end) ac.onFolderChanged(ac.getFolder(ac.FolderID.ExtCfgUser), '{ ?ffb_tweaks.ini | ?general.ini }', false, function () if ui.time() < ignoreChangesUntil then return end ac.log('Reload CSP controls settings') cfgFFBTweaks:reload() cfgCSPGeneral:reload() end) -- Drawing curves: ---@param size vec2 ---@return vec2 ---@return vec2 local function drawCurveBase(size) ui.offsetCursorY(4) local from, range = ui.getCursor(), size:clone() ui.dummy(range) ui.pushFont(ui.Font.Tiny) ui.offsetCursorX(4) ui.text('0%') ui.sameLine(0, (range.x - 20) / 2 - 10) ui.text('Input') ui.sameLine(0, (range.x - 20) / 2 - 24) ui.text('100%') local c = ui.getCursor() from.x = from.x + 20 range.x = range.x - 20 ui.drawRectFilled(from, from + range, rgbm(0, 0, 0, 0.5)) ui.setCursor(from + vec2(-20, range.y / 2)) ui.beginRotation() ui.text('Output') ui.endRotation(180) ui.setCursor(from + vec2(-21, 0)) ui.text('100%') ui.popFont() from.y, range.y = from.y + range.y, -range.y ui.setCursor(c) ui.offsetCursorY(8) return from, range end local function loadCurve(curve) local r = {} local c = io.load(ac.getFolder(ac.FolderID.Cfg)..'/'..curve) if not c then return {} end for _, line in ipairs(io.load(ac.getFolder(ac.FolderID.Cfg)..'/'..curve):split('\n')) do local m = line:match('^[^;#/]+') if m then local p = m:split('|') local k, v = tonumber(p[1]), tonumber(p[2]) if k and v then table.insert(r, vec2(k, v)) end end end return r end local function drawCurve(curve, label, size) local c = table.getOrCreate(curvesCache, curve, loadCurve, curve) ui.text(label or 'Curve:') if not c or #c < 2 then ui.text('Curve is missing or damaged') return end local f, s = drawCurveBase(size or vec2(300, 120)) for i = 1, #c do ui.pathLineTo(f + c[i] * s) end ui.pathStroke(ac.getUI().accentColor) end local function drawGammaCurve(value, label) ui.textWrapped(label or 'Curve:', 300) value = value or cfgFFPostProcess.data.GAMMA.VALUE local f, s = drawCurveBase(vec2(300, 120)) for i = 0, 30 do local x = (i / 30) ^ 2 ui.pathLineTo(f + vec2(x, x ^ value) * s) end ui.pathStroke(ac.getUI().accentColor) end -- Few helper functions: ---@param key string local function axleValue(key) local s = cfgControls.data[key] if not s.MIN then return ac.getJoystickAxisValue(s.JOY, s.AXLE) end return math.abs(math.lerpInvSat(ac.getJoystickAxisValue(s.JOY, s.AXLE), s.MIN, s.MAX)) end ---Converts CSV file from WheelCheck to a LUT file. ---@param csvFile string|string[] @Filename. ---@return vec2[] local function convertCurveFromCSV(csvFile, smoothingSteps, intensity) local t = table.map(io.load(type(csvFile) == 'string' and csvFile or csvFile[1]):split('\n'), function (l) local k, v = l:match('([-%d.]+), [-%d.]+, [-%d.]+, ([-%d.]+)') k, v = tonumber(k), tonumber(v) return k and v and { tonumber(v), 1 }, tonumber(k) end) if type(csvFile) == 'table' then for i = 2, #csvFile do for _, l in ipairs(io.load(csvFile[i]):split('\n')) do local k, v = l:match('([-%d.]+), [-%d.]+, [-%d.]+, ([-%d.]+)') k, v = tonumber(k), tonumber(v) if t[k] then t[k][1], t[k][2] = t[k][1] + v, t[k][2] + 1 end end end end t = table.map(t, function (v, k) return {k, v[1] / v[2]} end) table.sort(t, function (a, b) return a[1] < b[1] end) local mf = table.maxEntry(t, function (i) return i[1] end)[1] local mo = table.maxEntry(t, function (i) return i[2] end)[2] local r = table.map(t, function (v) return #v > 0 and vec2(v[2] / mo, v[1] / mf) or nil end) ---@type vec2 for _ = 1, smoothingSteps do for i = 2, #r do r[i - 1].x, r[i].x = math.lerp(r[i - 1].x, r[i].x, 0.4), math.lerp(r[i - 1].x, r[i].x, 0.6) end r[1].x, r[#r].x = 0, 1 end table.sort(r, function (a, b) return a.x < b.x end) if intensity then for i = 1, #r do r[i].y = math.lerp(r[i].x, r[i].y, intensity) end end return r end ---@param curve vec2[] ---@return string local function curveToLUT(curve) return table.join(curve, '\n', function (i) return i.x..'|'..i.y end) end local function importNewCurve() os.openFileDialog({ defaultFolder = ac.getFolder(ac.FolderID.Cfg), fileTypes = { { name = 'LUT files', mask = '*.lut' }, { name = 'WheelCheck CSV tables', mask = '*.csv' } }, }, function (err, filename) if not err and filename and io.exists(filename) then local contents, fileName = io.load(filename) if not contents or #contents == 0 then ui.toast(ui.Icons.Warning, 'Failed to set a curve, file is empty') return end fileName = filename:gsub('.*[/\\\\]', '') if fileName:lower():match('%.csv$') then fileName = fileName:gsub('%.csv$', '.lut') contents = curveToLUT(convertCurveFromCSV(filename, 10)) if not contents or #contents == 0 then ui.toast(ui.Icons.Warning, 'Failed to convert a curve to LUT format') return end end local newDestination = ac.getFolder(ac.FolderID.Cfg)..'/'..fileName if io.exists(newDestination) then if contents == io.load(newDestination) then cfgFFPostProcess:set('LUT', 'CURVE', fileName) ui.toast(ui.Icons.Confirm, 'File with the same name and contents has been selected') return end newDestination = nil for i = 1, 1000 do local candidate = string.format('%s/%s-%d.lut', ac.getFolder(ac.FolderID.Cfg), fileName:gsub('%.lut$', ''), i) if not io.exists(candidate) then newDestination = candidate break end end if not newDestination then ui.toast(ui.Icons.Warning, 'Failed to find a new file name for a curve') return end end ignoreChangesUntil = ui.time() + 5 if io.save(newDestination, contents) then cfgFFPostProcess:set('LUT', 'CURVE', fileName) ui.toast(ui.Icons.Confirm, 'Curve file has been copied to AC configs folder') reloadCurves() else ui.toast(ui.Icons.Warning, 'Failed to copy curve file to AC configs folder') end end end) end local createLUTData ---@type {csv: string[], selected: table, smoothingSteps: integer, intensity: number, demoApplied: boolean} local function resetCreateLUTDataTest(withControlReload) if createLUTData and createLUTData.demoApplied then local cfg = ac.INIConfig.load(ac.getFolder(ac.FolderID.Cfg)..'/ff_post_process.ini') cfg:setAndSave('HEADER', 'ENABLED', cfgFFPostProcess.data.HEADER.ENABLED) cfg:setAndSave('HEADER', 'TYPE', cfgFFPostProcess.data.HEADER.TYPE) cfg:setAndSave('LUT', 'CURVE', cfgFFPostProcess.data.LUT.CURVE) if withControlReload then ac.reloadControlSettings() end createLUTData.demoApplied = false end end onConfigChange = function() resetCreateLUTDataTest() end function script.windowEndLUTCreation() resetCreateLUTDataTest(true) end function script.windowCreateLUT() if not createLUTData then createLUTData = { csv = io.scanDir(ac.getFolder(ac.FolderID.Documents), 'log2 *.csv'), selected = {}, smoothingSteps = 10, intensity = 1, demoApplied = false } table.sort(createLUTData.csv, function (a, b) return a > b end) end ui.pushFont(ui.Font.Small) ui.header('Found tables (newest first):') local selected = {} ui.childWindow('tablesList', vec2(0, 120), function () for _, v in ipairs(createLUTData.csv) do if ui.checkbox(v, createLUTData.selected[v] or false) then createLUTData.selected[v] = not createLUTData.selected[v] createLUTData.generatedCurve = nil end if createLUTData.selected[v] then selected[#selected + 1] = v end end end) ui.setNextItemWidth(ui.availableSpaceX()) createLUTData.smoothingSteps = ui.slider('##smoothing', createLUTData.smoothingSteps, 1, 20, 'Smoothing steps: %.0f') if ui.itemEdited() then createLUTData.generatedCurve = nil end ui.setNextItemWidth(ui.availableSpaceX()) createLUTData.intensity = ui.slider('##intensity', createLUTData.intensity * 100, 0, 200, 'Intensity: %.0f%%') / 100 if ui.itemEdited() then createLUTData.generatedCurve = nil end local updateTest if createLUTData.generatedCurve == nil then updateTest = createLUTData.demoApplied if #selected == 0 then createLUTData.generatedCurve = false else createLUTData.generatedCurve = convertCurveFromCSV(table.map(selected, function (item) return ac.getFolder(ac.FolderID.Documents)..'/'..item end), createLUTData.smoothingSteps, createLUTData.intensity) end end local c = createLUTData.generatedCurve or error() -- to stop Lua extension from complaining about possible nil if createLUTData.generatedCurve then local f, s = drawCurveBase(vec2(300, 280)) for i = 1, #c do ui.pathLineTo(f + c[i] * s) end ui.pathStroke(ac.getUI().accentColor) if ui.button('Test current curve', vec2(ui.availableSpaceX() / 2 - 2, 0), createLUTData.demoApplied and ui.ButtonFlags.Active or 0) then createLUTData.demoApplied = not createLUTData.demoApplied updateTest = true end ui.sameLine(0, 4) ui.button('Save and apply the curve', vec2(ui.availableSpaceX(), 0)) else ui.textWrapped('• Select some generated tables. If several tables are selected, they will be averaged;') ui.textWrapped('• To generate a table, you can use WheelCheck:') ui.offsetCursorX(20) if ui.button('Find WheelCheck') then os.openURL('https://www.racedepartment.com/downloads/lut-generator-for-ac.9740/') end ui.textWrapped('• Simply launch it and select “Step Log 2” in “Spring Force” dropdown, and wait for a bit until your wheel would stop moving, new table should appear in this list;') ui.textWrapped('• If WheelCheck doesn’t work, close Content Manager (when AC is running, it minimizes itself to Windows tray);') ui.textWrapped('• Or, you can use LUT Generator for AC from RaceDepartment, it might produce more accurate results.') end if updateTest then setTimeout(function () ignoreChangesUntil = ui.time() + 5 local cfg = ac.INIConfig.load(ac.getFolder(ac.FolderID.Cfg)..'/ff_post_process.ini') if not createLUTData.demoApplied then io.deleteFile(ac.getFolder(ac.FolderID.Cfg)..'/_curve_tmp.lut') cfg:setAndSave('HEADER', 'ENABLED', cfgFFPostProcess.data.HEADER.ENABLED) cfg:setAndSave('HEADER', 'TYPE', cfgFFPostProcess.data.HEADER.TYPE) cfg:setAndSave('LUT', 'CURVE', cfgFFPostProcess.data.LUT.CURVE) else if c then io.save(ac.getFolder(ac.FolderID.Cfg)..'/_curve_tmp.lut', curveToLUT(c)) else io.deleteFile(ac.getFolder(ac.FolderID.Cfg)..'/_curve_tmp.lut') end cfg:setAndSave('HEADER', 'ENABLED', not not c) cfg:setAndSave('HEADER', 'TYPE', 'LUT') cfg:setAndSave('LUT', 'CURVE', c and '_curve_tmp.lut' or '') end ac.reloadControlSettings() end, 0.04, 'curveTest') end ui.popFont() end -- Editing controls: ---@param cfg MappedConfig ---@param section string ---@param key string ---@param from number ---@param to number ---@param mult number ---@param format string ---@param tooltip string|function|nil local function slider(cfg, section, key, from, to, mult, format, tooltip, preprocess) if not cfg.data[section] then error('No such section: '..section, 2) end if not cfg.data[section][key] then error('No such key: '..key, 2) end if settings.higherPrecision then format = format:gsub('%.0f%%', '%.1f%%') end local curValue = ui.slider('##'..section..key, mult < 0 and -mult / cfg.data[section][key] or cfg.data[section][key] * mult, from, to, format) if tooltip and (ui.itemHovered() or ui.itemActive()) then (type(tooltip) == 'function' and ui.tooltip or ui.setTooltip)(tooltip) end if preprocess then curValue = preprocess(curValue) end ui.sameLine(0, 4) local changed = math.abs(cfg.original[section][key] - (mult < 0 and -mult / curValue or curValue / mult)) > 0.0001 if ui.button('##r'..section..key, vec2(20, 20), changed and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then curValue = mult < 0 and -mult / cfg.original[section][key] or cfg.original[section][key] * mult end ui.addIcon(ui.Icons.Restart, 10, 0.5, nil, 0) if ui.itemHovered() then local v = string.format(format:match('%%.+'), mult < 0 and -mult / cfg.original[section][key] or cfg.original[section][key] * mult) ui.setTooltip(string.format(changed and 'Click to restore original value: %s' or 'Original value: %s', v)) end local rounded = settings.higherPrecision and math.floor(curValue * 10) / 10 or math.floor(curValue) cfg:set(section, key, mult < 0 and -mult / curValue or rounded / mult, true) end ---@param cfg MappedConfig ---@param section string ---@param key string ---@param label string ---@param tooltip string? local function checkbox(cfg, section, key, label, tooltip) if cfg.data[section] == nil then error('No such section: '..section, 2) end if cfg.data[section][key] == nil then error('No such key: '..key, 2) end local curValue = cfg.data[section][key] if ui.checkbox(label, curValue) then curValue = not curValue end if tooltip and ui.itemHovered() then (type(tooltip) == 'function' and ui.tooltip or ui.setTooltip)(tooltip) end ui.sameLine(0, 0) ui.offsetCursorX(ui.availableSpaceX() - 20) local changed = cfg.original[section][key] ~= curValue if ui.button('##r'..section..key, vec2(20, 20), changed and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then curValue = cfg.original[section][key] end ui.addIcon(ui.Icons.Restart, 10, 0.5, nil, 0) if ui.itemHovered() then ui.setTooltip(string.format(changed and 'Click to restore original value: %s' or 'Original value: %s', cfg.original[section][key] and 'yes' or 'no')) end cfg:set(section, key, curValue, true) end local function combo(cfg, section, key, label, previewCallback, contentCallback, tooltip, widthOpt) ui.alignTextToFramePadding() ui.text(label..':') ui.sameLine(widthOpt or 68, 0) local current = cfgControls.data[section][key] local id = section..'/'..key ui.setNextItemWidth(ui.availableSpaceX() - 24) ui.combo('##r'..section..key, previewCallback(current), ui.ComboFlags.None, contentCallback) if tooltip and ui.itemHovered() then (type(tooltip) == 'function' and ui.tooltip or ui.setTooltip)(tooltip) end ui.sameLine(0, 4) local changed = cfgControls.original[section][key] ~= current if ui.button('##r'..id, vec2(20, 20), changed and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then cfgControls:set(section, key, cfgControls.original[section][key], true, true) end ui.addIcon(ui.Icons.Restart, 10, 0.5, nil, 0) if ui.itemHovered() then ui.setTooltip(string.format(changed and 'Click to restore original value: %s' or 'Original value: %s', previewCallback(cfgControls.original[section][key]))) end end local keyNames = require('keys') local keyWaiting local function keyboardButton(section, key, label, tooltip) ui.alignTextToFramePadding() ui.text(label..':') ui.sameLine(120, 0) local current = cfgControls.data[section][key] local id = section..'/'..key if ui.button(string.format('%s###%s', keyNames[current] or current < 1 and 'None' or current, id), vec2(ui.availableSpaceX() - 24, 0), keyWaiting == id and ui.ButtonFlags.Active or 0) then keyWaiting = keyWaiting ~= id and id or nil end if tooltip and ui.itemHovered() then (type(tooltip) == 'function' and ui.tooltip or ui.setTooltip)(tooltip) end ui.sameLine(0, 4) local changed = cfgControls.original[section][key] ~= current if ui.button('##r'..id, vec2(20, 20), changed and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then cfgControls:set(section, key, cfgControls.original[section][key], true, true) keyWaiting = nil end ui.addIcon(ui.Icons.Restart, 10, 0.5, nil, 0) if ui.itemHovered() then local v = keyNames[cfgControls.original[section][key]] or cfgControls.original[section][key] < 1 and 'None' or current ui.setTooltip(string.format(changed and 'Click to restore original value: %s' or 'Original value: %s', v)) end if keyWaiting == id then if ui.keyboardButtonDown(ui.KeyIndex.Escape) or ui.keyboardButtonDown(ui.KeyIndex.Back) then keyWaiting = nil else for k, _ in pairs(keyNames) do if ui.keyboardButtonDown(k) then cfgControls:set(section, key, k, true, true) keyWaiting = nil end end end end end local gamepadNames = { ['DPAD_LEFT'] = { ac.GamepadButton.DPadLeft, 'D-Pad Left' }, ['DPAD_RIGHT'] = { ac.GamepadButton.DPadRight, 'D-Pad Right' }, ['DPAD_UP'] = { ac.GamepadButton.DPadUp, 'D-Pad Up' }, ['DPAD_DOWN'] = { ac.GamepadButton.DpadLeft, 'D-Pad Down' }, ['A'] = { ac.GamepadButton.A, 'A' }, ['B'] = { ac.GamepadButton.B, 'B' }, ['X'] = { ac.GamepadButton.X, 'X' }, ['Y'] = { ac.GamepadButton.Y, 'Y' }, ['LSHOULDER'] = { ac.GamepadButton.LeftShoulder, 'Left Shoulder' }, ['RSHOULDER'] = { ac.GamepadButton.RightShoulder, 'Right Shoulder' }, ['LTHUMB_PRESS'] = { ac.GamepadButton.LeftThumb, 'Left Thumb' }, ['RTHUMB_PRESS'] = { ac.GamepadButton.RightThumb, 'Right Thumb' }, ['START'] = { ac.GamepadButton.Start, 'Start' }, ['BACK'] = { ac.GamepadButton.Back, 'Back' }, } local gamepadWaiting local function gamepadButton(section, label, tooltip) ui.alignTextToFramePadding() ui.text(label..':') ui.sameLine(120, 0) local key = 'XBOXBUTTON' local current = cfgControls.data[section][key] local id = section..'/'..key if ui.button(string.format('%s###%s', gamepadNames[current] and gamepadNames[current][2] or 'None', id), vec2(ui.availableSpaceX() - 24, 0), gamepadWaiting == id and ui.ButtonFlags.Active or 0) then gamepadWaiting = gamepadWaiting ~= id and id or nil end if tooltip and ui.itemHovered() then (type(tooltip) == 'function' and ui.tooltip or ui.setTooltip)(tooltip) end ui.sameLine(0, 4) local changed = cfgControls.original[section][key] ~= current if ui.button('##r'..id, vec2(20, 20), changed and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then cfgControls:set(section, key, cfgControls.original[section][key], true) gamepadWaiting = nil end ui.addIcon(ui.Icons.Restart, 10, 0.5, nil, 0) if ui.itemHovered() then local v = gamepadNames[cfgControls.original[section][key]] and gamepadNames[cfgControls.original[section][key]][2] or 'None' ui.setTooltip(string.format(changed and 'Click to restore original value: %s' or 'Original value: %s', v)) end if gamepadWaiting == id then if ui.keyboardButtonDown(ui.KeyIndex.Escape) or ui.keyboardButtonDown(ui.KeyIndex.Back) then gamepadWaiting = nil elseif ui.keyboardButtonDown(ui.KeyIndex.Delete) then cfgControls:set(section, key, '', true) gamepadWaiting = nil else for k, _ in pairs(gamepadNames) do if ac.isGamepadButtonPressed(cfgControls.data.X360.JOYPAD_INDEX, _[1]) then cfgControls:set(section, key, k, true) gamepadWaiting = nil end end end end end local function mouseButton(section, key, label, tooltip) ui.alignTextToFramePadding() ui.text(label..':') ui.sameLine(120, 0) local current = cfgControls.data[section][key] local keys = { [0] = 'None', 'Left', 'Right', 'Middle', 'Fourth', 'Fifth' } local id = section..'/'..key ui.setNextItemWidth(ui.availableSpaceX() - 24) ui.combo(string.format('##%s', id), keys[current] or 'None', ui.ComboFlags.None, function () for i = 0, #keys do local v = keys[i] if ui.selectable(v, i == 0 and current < 1 or i == current) then cfgControls:set(section, key, i == 0 and -1 or i, true) end end end) if tooltip and ui.itemHovered() then (type(tooltip) == 'function' and ui.tooltip or ui.setTooltip)(tooltip) end ui.sameLine(0, 4) local changed = cfgControls.original[section][key] ~= current if ui.button('##r'..id, vec2(20, 20), changed and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then cfgControls:set(section, key, cfgControls.original[section][key], true, true) keyWaiting = nil end ui.addIcon(ui.Icons.Restart, 10, 0.5, nil, 0) if ui.itemHovered() then ui.setTooltip(string.format(changed and 'Click to restore original value: %s' or 'Original value: %s', keys[cfgControls.original[section][key]] or 'None')) end end -- Specialized controls: local originalFFBGain = tonumber(ac.load('app.ControllerTweaks:originalFFBGain')) or car.ffbMultiplier ac.store('app.ControllerTweaks:originalFFBGain', originalFFBGain) local function userFFBGainSlider() ui.setNextItemWidth(ui.availableSpaceX() - 48) local curValue = ui.slider('##ffb', car.ffbMultiplier * 100, 0, 200, 'Car FFB gain: %.0f%%') / 100 ui.sameLine(0, 4) local changed = 1 ~= curValue if ui.button('##ffbResetTo100', vec2(20, 20), changed and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then curValue = 1 end ui.addIcon(ui.Icons.Cancel, 10, 0.5, nil, 0) if ui.itemHovered() then ui.setTooltip(changed and 'Click to set FFB gain to 100%' or 'Base value: 100%') end ui.sameLine(0, 4) changed = originalFFBGain ~= curValue if ui.button('##ffbReset', vec2(20, 20), changed and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then curValue = originalFFBGain end ui.addIcon(ui.Icons.Restart, 10, 0.5, nil, 0) if ui.itemHovered() then ui.setTooltip(string.format(changed and 'Click to restore original FFB gain: %.0f%%' or 'Original value: %.0f%%', originalFFBGain * 100)) end if curValue ~= car.ffbMultiplier then ac.setFFBMultiplier(curValue) end end local function getGyroMode() return cfgFFBTweaks.data.BASIC.ENABLED and cfgFFBTweaks.data.GYRO2.ENABLED and 3 or cfgSystem.data.FF_EXPERIMENTAL.ENABLE_GYRO and 2 or 1 end local originalGyroMode = tonumber(ac.load('app.ControllerTweaks:originalGyroMode')) or getGyroMode() ac.store('app.ControllerTweaks:originalGyroMode', originalGyroMode) local function gyroModeCombo() ui.alignTextToFramePadding() ui.text('Mode:') ui.sameLine(68) ui.setNextItemWidth(ui.availableSpaceX() - 24) local currentGyroMode, changed = getGyroMode(), nil currentGyroMode, changed = ui.combo('##gyro', currentGyroMode, ui.ComboFlags.None, cfgFFBTweaks.data.BASIC.ENABLED and { 'None', 'Standard', 'FFB Tweaks' } or { 'None', 'Standard' }) ui.sameLine(0, 4) local changedFromOriginal = originalGyroMode ~= currentGyroMode if ui.button('##gyroReset', vec2(20, 20), changedFromOriginal and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then currentGyroMode, changed = originalGyroMode, true end ui.addIcon(ui.Icons.Restart, 10, 0.5, nil, 0) if ui.itemHovered() then ui.setTooltip(string.format(changedFromOriginal and 'Click to restore original gyro mode setting: %s' or 'Original gyro mode setting: %s', originalGyroMode == 3 and 'FFB Tweaks' or originalGyroMode == 2 and 'standard' or 'none')) end if changed then cfgSystem:set('FF_EXPERIMENTAL', 'ENABLE_GYRO', currentGyroMode == 2) cfgFFBTweaks:set('GYRO2', 'ENABLED', currentGyroMode == 3, false) end if currentGyroMode == 3 then slider(cfgFFBTweaks, 'GYRO2', 'STRENGTH', 0, 100, 100, 'Gyro strength: %.0f%%', 'Effect strength') end end local function getPPMode() return cfgFFPostProcess.data.HEADER.ENABLED and (cfgFFPostProcess.data.HEADER.TYPE == 'GAMMA' and 2 or 3) or 1 end local originalPPMode = tonumber(ac.load('app.ControllerTweaks:originalPPMode')) or getPPMode() ac.store('app.ControllerTweaks:originalPPMode', originalPPMode) local originalCurve = tonumber(ac.load('app.ControllerTweaks:originalCurve')) or cfgFFPostProcess.original.LUT.CURVE ac.store('app.ControllerTweaks:originalCurve', originalCurve) local function ppModeCombo() ui.alignTextToFramePadding() ui.text('Mode:') ui.sameLine(68) ui.setNextItemWidth(ui.availableSpaceX() - 24) local currentPPMode, changed = getPPMode(), false ui.combo('##pp', currentPPMode == 3 and 'LUT' or currentPPMode == 2 and 'Gamma' or 'Disabled', ui.ComboFlags.None, function () if ui.selectable('Disabled') then currentPPMode, changed = 1, true end if ui.itemHovered() then ui.tooltip(function () drawGammaCurve(1) end) end if ui.selectable('Gamma') then currentPPMode, changed = 2, true end if ui.itemHovered() then ui.tooltip(function () drawGammaCurve() end) end if ui.selectable('LUT') then currentPPMode, changed = 3, true end if ui.itemHovered() then ui.tooltip(function () drawCurve(cfgFFPostProcess.data.LUT.CURVE) end) end end) ui.sameLine(0, 4) local changedFromOriginal = originalPPMode ~= currentPPMode if ui.button('##ppReset', vec2(20, 20), changedFromOriginal and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then currentPPMode, changed = originalPPMode, true end ui.addIcon(ui.Icons.Restart, 10, 0.5, nil, 0) if ui.itemHovered() then ui.setTooltip(string.format(changedFromOriginal and 'Click to restore original post-processing mode setting: %s' or 'Original post-processing mode setting: %s', originalPPMode == 3 and 'LUT' or originalPPMode == 2 and 'gamma' or 'none')) end if changed then cfgFFPostProcess:set('HEADER', 'ENABLED', currentPPMode ~= 1) if currentPPMode ~= 1 then cfgFFPostProcess:set('HEADER', 'TYPE', currentPPMode == 2 and 'GAMMA' or 'LUT') end end if currentPPMode == 2 then slider(cfgFFPostProcess, 'GAMMA', 'VALUE', 30, 300, 100, 'Gamma value: %.0f%%', function () ui.tooltip(function () drawGammaCurve(nil, 'Decrease above 100% to boost smaller forces, increase above 100% to attenuate smaller forces:') end) end, function (v) return math.max(v, 30) end) elseif currentPPMode == 3 then ui.alignTextToFramePadding() ui.text('Curve:') ui.sameLine(68) ui.setNextItemWidth(ui.availableSpaceX() - 24) ui.combo('##ppFile', cfgFFPostProcess.data.LUT.CURVE or 'None', function () for i = 1, #curves do if ui.selectable(curves[i], curves[i] == cfgFFPostProcess.data.LUT.CURVE) then cfgFFPostProcess:set('LUT', 'CURVE', curves[i]) end if ui.itemHovered() then ui.tooltip(function () drawCurve(curves[i]) end) end end ui.separator() if ui.selectable('Add new curve from LUT…') then importNewCurve() end if ui.itemHovered() then ui.setTooltip('Select a LUT file and it will be copied to “Documents/Assetto Corsa/cfg” and selected') end if ui.selectable('Create new curve using WheelCheck…') then ac.setWindowOpen('createLUT', true) end if ui.itemHovered() then ui.setTooltip('Create new curve for your steering wheel using WheelCheck') end end) if ui.itemHovered() and cfgFFPostProcess.data.LUT.CURVE then ui.tooltip(function () drawCurve(cfgFFPostProcess.data.LUT.CURVE) end) end ui.sameLine(0, 4) if ui.button('##curveReset', vec2(20, 20), originalCurve ~= cfgFFPostProcess.data.LUT.CURVE and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then cfgFFPostProcess:set('LUT', 'CURVE', originalCurve) end ui.addIcon(ui.Icons.Restart, 10, 0.5, nil, 0) if ui.itemHovered() then ui.setTooltip(string.format(originalCurve ~= cfgFFPostProcess.data.LUT.CURVE and 'Click to restore original curve: %s' or 'Original curve: %s', originalCurve)) end end end -- UI for different input modes: local configurators = {} function configurators.KEYBOARD() ui.header('Keyboard steering:') slider(cfgControls, 'KEYBOARD', 'STEERING_SPEED', 0, 400, 100, 'Speed: %.0f%%') slider(cfgControls, 'KEYBOARD', 'STEERING_OPPOSITE_DIRECTION_SPEED', 0, 400, 100, 'Opposite direction speed: %.0f%%', 'Speed of steering when steering back to zero') slider(cfgControls, 'KEYBOARD', 'STEER_RESET_SPEED', 0, 400, 100, 'Reset speed: %.0f%%', 'Speed of automatic return to zero') ui.offsetCursorY(16) ui.header('Mouse steering:') checkbox(cfgControls, 'KEYBOARD', 'MOUSE_STEER', 'Mouse steering', 'Use Ctrl+M to get cursor pointer back') checkbox(cfgControls, 'KEYBOARD', 'MOUSE_ACCELERATOR_BRAKE', 'Mouse acceleration and braking', 'Use mouse buttons to accelerate and brake') checkbox(cfgCSPGeneral, 'CONTROL', 'NO_MOUSE_STEERING_FOR_INACTIVE', 'No mouse steering in background', 'Stops mouse steering (including mouse capture) if Assetto Corsa is not in foreground') slider(cfgControls, 'KEYBOARD', 'MOUSE_SPEED', 0, 300, -10, 'Speed: %.0f%%', nil, function (v) return math.max(v, 0.01) end) checkbox(cfgControls, '__EXT_KEYBOARD', 'SHIFT_WITH_WHEEL', 'Shift gears with mouse wheel', 'Use mouse wheel to change gears') checkbox(cfgControls, '__EXT_KEYBOARD', 'SHIFT_WITH_XBUTTONS', 'Shift with 4th and 5th buttons', 'Use additional mouse buttons to change gears') ui.offsetCursorY(16) ui.header('Buttons:') keyboardButton('KEYBOARD', 'GAS', 'Gas') keyboardButton('KEYBOARD', 'BRAKE', 'Brake') keyboardButton('KEYBOARD', 'RIGHT', 'Right') keyboardButton('KEYBOARD', 'LEFT', 'Left') keyboardButton('KEYBOARD', 'GEAR_UP', 'Gear up') keyboardButton('KEYBOARD', 'GEAR_DOWN', 'Gear down') keyboardButton('KEYBOARD', 'HANDBRAKE', 'Handbrake') ui.offsetCursorY(16) ui.header('Forced throttle:') checkbox(cfgControls, '__EXT_KEYBOARD_GAS_RAW', 'OVERRIDE', 'Enable forced throttle', 'A CSP feature allowing to bind alternative throttle buttons overriding built-in traction control') keyboardButton('__EXT_KEYBOARD_GAS_RAW', 'KEY', 'Full throttle', 'If button is pressed, full throttle is applied') keyboardButton('__EXT_KEYBOARD_GAS_RAW', 'KEY_MODIFICATOR', 'Throttle modifier', 'If gas button and this button both are pressed, full throttle is applied') mouseButton('__EXT_KEYBOARD_GAS_RAW', 'MOUSE', 'Full throttle', 'If mouse button is pressed, full throttle is applied') mouseButton('__EXT_KEYBOARD_GAS_RAW', 'MOUSE_MODIFICATOR', 'Throttle modifier', 'If gas button and this mouse button both are pressed, full throttle is applied') slider(cfgControls, '__EXT_KEYBOARD_GAS_RAW', 'LAG_UP', 0, 99, 100, 'Filter (up): %.0f%%', 'Filter for forced throttle to kick in') slider(cfgControls, '__EXT_KEYBOARD_GAS_RAW', 'LAG_DOWN', 0, 99, 100, 'Filter (down): %.0f%%', 'Filter for forced throttle to turn off') ui.offsetCursorY(16) ui.header('Pedals state:') ui.pushStyleColor(ui.StyleColor.PlotHistogram, rgbm(0, 0.4, 0, 1)) ui.progressBar(car.gas, vec2(ui.availableSpaceX(), 0), 'Gas') ui.popStyleColor() ui.pushStyleColor(ui.StyleColor.PlotHistogram, rgbm(0.4, 0, 0, 1)) ui.progressBar(car.brake, vec2(ui.availableSpaceX(), 0), 'Brake') ui.popStyleColor() end local gamepadButtonsOrdered = { { 'GEARUP', 'Next gear' }, { 'GEARDN', 'Previous gear' }, { 'HANDBRAKE', 'Handbrake' }, { '__EXT_KEYBOARD_CLUTCH', 'Clutch' }, { 'ACTION_HEADLIGHTS', 'Headlights' }, 'Additional', { 'ACTION_HORN', 'Horn' }, { 'GLANCEBACK', 'Look back' }, { 'GLANCELEFT', 'Look left' }, { 'GLANCERIGHT', 'Look right' }, { 'ACTION_CHANGE_CAMERA', 'Change camera' }, { 'DRIVER_NAMES', 'Driver names' }, { '__EXT_SIM_PAUSE', 'Pause' }, 'Car control', { 'ABSDN', 'ABS (reduce)' }, { 'ABSUP', 'ABS (increase)' }, { 'TCDN', 'TC (reduce)' }, { 'TCUP', 'TC (increase)' }, { 'TURBODN', 'Turbo (reduce)' }, { 'TURBOUP', 'Turbo (increase)' }, { 'KERS', 'KERS' }, { 'DRS', 'DRS' }, -- { 'ABS', 'ABS' }, -- { 'TRACTION_CONTROL', 'Traction control' }, -- { 'ACTION_HEADLIGHTS_FLASH', 'Headlights (flash)' }, -- { 'BALANCEDN', 'BALANCEDN' }, -- { 'BALANCEUP', 'BALANCEUP' }, -- { 'ENGINE_BRAKE_DN', 'ENGINE_BRAKE_DN' }, -- { 'ENGINE_BRAKE_UP', 'ENGINE_BRAKE_UP' }, -- { 'MGUH_MODE', 'MGUH_MODE' }, -- { 'MGUK_DELIVERY_DN', 'MGUK_DELIVERY_DN' }, -- { 'MGUK_DELIVERY_UP', 'MGUK_DELIVERY_UP' }, -- { 'MGUK_RECOVERY_DN', 'MGUK_RECOVERY_DN' }, -- { 'MGUK_RECOVERY_UP', 'MGUK_RECOVERY_UP' }, -- { 'ACTION_CELEBRATE', 'ACTION_CELEBRATE' }, -- { 'ACTION_CLAIM', 'ACTION_CLAIM' }, -- { 'ACTIVATE_AI', 'ACTIVATE_AI' }, -- { 'AUTO_SHIFTER', 'AUTO_SHIFTER' }, -- { 'HIDE_APPS', 'HIDE_APPS' }, -- { 'HIDE_DAMAGE', 'HIDE_DAMAGE' }, -- { 'SHOW_DAMAGE', 'SHOW_DAMAGE' }, -- { 'IDEAL_LINE', 'IDEAL_LINE' }, -- { 'MOUSE_STEERING', 'MOUSE_STEERING' }, -- { 'FFWD', 'Fast forward' }, -- { 'NEXT_CAR', 'NEXT_CAR' }, -- { 'NEXT_LAP', 'NEXT_LAP' }, -- { 'PAUSE_REPLAY', 'PAUSE_REPLAY' }, -- { 'PLAYER_CAR', 'PLAYER_CAR' }, -- { 'PREVIOUS_CAR', 'PREVIOUS_CAR' }, -- { 'PREVIOUS_LAP', 'PREVIOUS_LAP' }, -- { 'SLOWMO', 'SLOWMO' }, -- { 'STARTER', 'STARTER' }, -- { 'START_REPLAY', 'START_REPLAY' }, -- { 'REV', 'REV' }, -- { 'RESET_RACE', 'RESET_RACE' }, } function configurators.X360() --[[ local index = cfgControls.data.X360.JOYPAD_INDEX % 4 local useDualSense = cfgControls.data.X360.JOYPAD_INDEX > 3 index = ui.slider('##gamepad', index + 1, 1, 4, 'Gamepad: %.0f') - 1 local changed = ui.itemEdited() if ui.checkbox('Use PS 5 DualSense gamepad', useDualSense) then useDualSense, changed = not useDualSense, true end if changed then cfgControls:set('X360', 'JOYPAD_INDEX', index + (useDualSense and 4 or 0), true) end ui.offsetCursorY(16) ]] ui.header('Steering:') combo(cfgControls, 'X360', 'STEER_THUMB', 'Thumb', function (v) return v == 'LEFT' and 'Left' or 'Right' end, function () if ui.selectable('Left') then cfgControls:set('X360', 'STEER_THUMB', 'LEFT', true) end if ui.selectable('Right') then cfgControls:set('X360', 'STEER_THUMB', 'RIGHT', true) end end, 'If you are using Neck FX, you might need to change thumb used for glancing around in its settings too') slider(cfgControls, 'X360', 'STEER_SPEED', 0, 200, 100, 'Speed: %.0f%%', 'Base speed of turning the steering wheel') slider(cfgControls, 'X360', 'STEER_GAMMA', 100, 500, 100, 'Gamma: %.0f%%', function () ui.text('Curve (use steering thumb to see it in action):') local f, s = drawCurveBase(vec2(300, 120)) local b = ac.getGamepadAxisValue(0, cfgControls.data.X360.STEER_THUMB == 'LEFT' and ac.GamepadAxis.LeftThumbX or ac.GamepadAxis.RightThumbX) if math.abs(b) > 0.01 then local relSteer = car.steer / 396 ui.drawLine(f + vec2(0, (0.5 + 0.5 * relSteer) * s.y), f + vec2(s.x, (0.5 + 0.5 * relSteer) * s.y), rgbm.colors.gray) ui.drawLine(f + vec2((0.5 + 0.5 * b) * s.x, 0), f + vec2((0.5 + 0.5 * b) * s.x, s.y), rgbm.colors.gray) end local v = cfgControls.data.X360.STEER_GAMMA if v == 0 then v = 1 end for i0 = 0, 1 do for i = 0, 30 do local x = (i / 30) ^ 2 if i0 == 1 then ui.pathLineTo(f + vec2(0.5 + 0.5 * x, 0.5 + 0.5 * x ^ v) * s) else ui.pathLineTo(f + vec2(0.5 - 0.5 * x, 0.5 - 0.5 * x ^ v) * s) end end ui.pathStroke(ac.getUI().accentColor) end end) slider(cfgControls, 'X360', 'STEER_FILTER', 0, 100, 100, 'Filter: %.0f%%', 'Smooths out steering angle') slider(cfgControls, 'X360', 'SPEED_SENSITIVITY', 0, 100, 100, 'Speed sensitivity: %.0f%%', 'Increase to reduce steering range with high speeds') slider(cfgControls, 'X360', 'STEER_DEADZONE', 0, 100, 100, 'Deadzone: %.0f%%', 'Ignore steering inputs within that range') slider(cfgControls, 'X360', 'RUMBLE_INTENSITY', 0, 100, 100, 'Rumble intensity: %.0f%%', 'How much gamepad would shake (if supported)') ui.offsetCursorY(16) ui.header('Buttons:') for i = 1, #gamepadButtonsOrdered do local v = gamepadButtonsOrdered[i] if type(v) == 'string' then ui.offsetCursorY(16) ui.header(v..':') else gamepadButton(v[1], v[2]) end end end function configurators.WHEEL() userFFBGainSlider() ui.offsetCursorY(16) ui.header('Steering:') slider(cfgControls, 'STEER', 'STEER_GAMMA', 20, 400, 100, 'Gamma: %.0f%%', function () ui.text('Curve (steer to see it in action):') local f, s = drawCurveBase(vec2(300, 120)) local b = axleValue('STEER') ui.pushClipRect(f + vec2(0, s.y), f + vec2(s.x, 0)) if math.abs(b) > 0.003 then local relSteer = car.steer / (cfgControls.data.STEER.LOCK / 2) ui.drawLine(f + vec2(0, (0.5 + 0.5 * relSteer) * s.y), f + vec2(s.x, (0.5 + 0.5 * relSteer) * s.y), rgbm.colors.gray) ui.drawLine(f + vec2((0.5 + 0.5 * b) * s.x, 0), f + vec2((0.5 + 0.5 * b) * s.x, s.y), rgbm.colors.gray) end local v = cfgControls.data.STEER.STEER_GAMMA if v == 0 then v = 1 end for i0 = 0, 1 do for i = 0, 30 do local x = (i / 30) ^ 2 if i0 == 1 then ui.pathLineTo(f + vec2(0.5 + 0.5 * x, 0.5 + 0.5 * x ^ v) * s) else ui.pathLineTo(f + vec2(0.5 - 0.5 * x, 0.5 - 0.5 * x ^ v) * s) end end ui.pathStroke(ac.getUI().accentColor) end ui.popClipRect() end) slider(cfgControls, 'STEER', 'SPEED_SENSITIVITY', 0, 100, 100, 'Speed sensitivity: %.0f%%', 'Reduces sensitivity when car accelerates') ui.offsetCursorY(16) ui.header('Others:') slider(cfgControls, 'STEER', 'DEBOUNCING_MS', 0, 200, 1, 'Debouncing: %.0f ms', 'Minimum delay between gear shifts with sequential gearbox') slider(cfgControls, 'BRAKES', 'GAMMA', 0, 500, 100, 'Brakes gamma: %.0f%%', function () ui.text('Curve (use brake pedal to see it in action):') local f, s = drawCurveBase(vec2(300, 120)) local b = axleValue('BRAKES') if b > 0.001 then ui.drawLine(f + vec2(0, car.brake * s.y), f + vec2(s.x, car.brake * s.y), rgbm.colors.gray) ui.drawLine(f + vec2(b * s.x, 0), f + vec2(b * s.x, s.y), rgbm.colors.gray) end local v = cfgControls.data.BRAKES.GAMMA if v == 0 then v = 1 end for i = 0, 30 do local x = (i / 30) ^ 2 ui.pathLineTo(f + vec2(x, x ^ v) * s) end ui.pathStroke(ac.getUI().accentColor) end ) ui.offsetCursorY(16) ui.header('FFB:') slider(cfgControls, 'STEER', 'FF_GAIN', 0, 200, 100, 'Gain: %.0f%%', 'Overall FFB intensity') slider(cfgControls, 'STEER', 'FILTER_FF', 0, 100, 100, 'Filter: %.0f%%', 'Increase to smooth out FFB') slider(cfgControls, 'FF_SKIP_STEPS', 'VALUE', 0, 10, 1, 'Skip steps: %.0f', string.format('Resulting FFB refresh rate: %.0f Hz', 333 / (1 + math.max(0, cfgControls.data.FF_SKIP_STEPS.VALUE)))) ui.offsetCursorY(16) ui.header('FFB tweaks:') slider(cfgControls, 'FF_TWEAKS', 'MIN_FF', 0, 100, 100, 'Minimum force: %.0f%%', 'Minimum force to be sent to the wheel (high values might introduce rattling at high speeds)') slider(cfgControls, 'FF_TWEAKS', 'CENTER_BOOST_GAIN', 0, 1000, 100, 'Center boost gain: %.0f%%', 'Specifies amount of optional FFB increase in the center of steering range') slider(cfgControls, 'FF_TWEAKS', 'CENTER_BOOST_RANGE', 0, 100, 100, 'Center boost range: %.0f%%', 'Specifies width of what constitutes a middle of that optional FFB increase in the center of steering range') ui.offsetCursorY(16) ui.header('FFB enhancements:') slider(cfgControls, 'FF_ENHANCEMENT', 'CURBS', 0, 200, 100, 'Curbs: %.0f%%', 'Adds vibrations when driving over curbs') slider(cfgControls, 'FF_ENHANCEMENT', 'ROAD', 0, 200, 100, 'Road: %.0f%%', 'Adds more vibrations for bumps in road surface') slider(cfgControls, 'FF_ENHANCEMENT', 'SLIPS', 0, 200, 100, 'Slips: %.0f%%', 'Adds vibrations when car is slipping') slider(cfgControls, 'FF_ENHANCEMENT', 'ABS', 0, 200, 100, 'ABS: %.0f%%', 'Adds vibrations when ABS is in action') if settings.showHidden then slider(cfgControls, 'FF_ENHANCEMENT_2', 'UNDERSTEER', 0, 200, 100, 'Understeer: %.0f%%', 'Fake effect reducing FFB when car is understeering') end ui.offsetCursorY(16) ui.header('FFB gyro:') gyroModeCombo() if settings.showHidden then ui.offsetCursorY(16) ui.header('Low speed FFB reduction:') slider(cfgSystem, 'LOW_SPEED_FF', 'SPEED_KMH', 0, 30, 1, 'Speed threshold: %.0f km/h', 'When driving slower than this, FFB will be lowered to prevent unnatural vibrations') slider(cfgSystem, 'LOW_SPEED_FF', 'MIN_VALUE', 0, 100, 100, 'Force multiplier: %.0f%%', 'Multiplier applied to FFB when driving slower than threshold') ui.offsetCursorY(16) ui.header('FFB damper:') slider(cfgSystem, 'FF_EXPERIMENTAL', 'DAMPER_GAIN', 0, 200, 100, 'Gain: %.0f%%', 'Adds stiffness to wheel when car is not moving fast (when car is stationary, gets cancelled out by low speed FFB reduction)') slider(cfgSystem, 'FF_EXPERIMENTAL', 'DAMPER_MIN_LEVEL', 0, 200, 100, 'Min level: %.0f%%', 'Amount of damping remaining for cars driving faster') end ui.offsetCursorY(16) ui.header('FFB post-processing:') if cfgFFBTweaks.data.BASIC.ENABLED then slider(cfgFFBTweaks, 'POSTPROCESSING', 'RANGE_COMPRESSION', 50, 400, 100, 'Range compression: %.0f%%', 'Set higher to compress force and make small forces easier to feel') if cfgFFBTweaks.data.POSTPROCESSING.RANGE_COMPRESSION ~= 1 then checkbox(cfgFFBTweaks, 'POSTPROCESSING', 'RANGE_COMPRESSION_ASSIST', 'Use car steer assist', 'Automatically convert STEER_ASSIST into range compression on cars that use STEER_ASSIST') end end ppModeCombo() end local vr = ac.getVR() function script.windowMain(dt) ui.pushFont(ui.Font.Small) ui.pushItemWidth(ui.availableSpaceX() - 24) if vr ~= nil and ac.getSim().isOpenVRMode then ui.header('OpenVR controller axis:') local w = (ui.availableSpaceX() - 16) / 4 for j = 0, 4 do ui.progressBar(0.5 + 0.5 * vr.hands[0].openVRAxis[j].x, vec2(w, 2), '') ui.sameLine(0, 4) ui.progressBar(0.5 + 0.5 * vr.hands[0].openVRAxis[j].y, vec2(w, 2), '') ui.sameLine(0, 8) ui.progressBar(0.5 + 0.5 * vr.hands[1].openVRAxis[j].x, vec2(w, 2), '') ui.sameLine(0, 4) ui.progressBar(0.5 + 0.5 * vr.hands[1].openVRAxis[j].y, vec2(w, 2), '') end ac.debug('0', vr.hands[1].openVRAxis[0].x) ui.offsetCursorY(20) end if configurators[cfgControls.data.HEADER.INPUT_METHOD] then configurators[cfgControls.data.HEADER.INPUT_METHOD]() else ui.textWrapped('Unknown input method: '..tostring(cfgControls.data.HEADER.INPUT_METHOD)) return end ui.popItemWidth() if settings.showDisclaimer then ui.offsetCursorY(16) if cfgControls.data.HEADER.INPUT_METHOD == 'WHEEL' and not cfgFFBTweaks.data.BASIC.ENABLED then ui.textWrapped('FFB Tweaks module is disabled. Click here to enable it and get more options:') if ui.button('Enable FFB Tweaks') then cfgFFBTweaks:set('BASIC', 'ENABLED', true) end elseif cfgControls.data.HEADER.INPUT_METHOD == 'WHEEL'then ui.textWrapped('Hold Shift when moving slider for more precision. Hold Ctrl and click on a slider to edit it. Please be careful if you are using a direct drive wheel, do not input crazy values.') elseif cfgControls.data.HEADER.INPUT_METHOD == 'KEYBOARD' then ui.textWrapped('Hold Shift when moving slider for more precision. Hold Ctrl and click on a slider to edit it. To toggle mouse steering during the race, use Ctrl+M.') else ui.textWrapped('Hold Shift when moving slider for more precision. Hold Ctrl and click on a slider to edit it.') end end ui.popFont() end function script.windowMainSettings(dt) ui.pushFont(ui.Font.Small) if ui.checkbox('Show hidden options', settings.showHidden) then settings.showHidden = not settings.showHidden end if ui.itemHovered() then ui.setTooltip('Hidden options are experimental and are not meant to be changed for most cases') end if ui.checkbox('Show disclaimer', settings.showDisclaimer) then settings.showDisclaimer = not settings.showDisclaimer end if ui.itemHovered() then ui.setTooltip('Please be careful when setting up direct drive wheel even if disclaimer is hidden') end if ui.checkbox('Higher precision', settings.higherPrecision) then settings.higherPrecision = not settings.higherPrecision end if ui.itemHovered() then ui.setTooltip('For some really fine tuning') end ui.offsetCursorY(16) ui.header('A few tips:') ui.textWrapped('• When app is opened, FFB settings reload automatically when configuration files change. You can use this app, but you can also use original launcher or Content Manager to edit FFB;', 400) ui.textWrapped('• Use “Create new curve…” in curve lists to generate new LUT curve using WheelCheck.', 400) ui.popFont() end -- function script.windowMain(dt) -- for i = 1, ac.getJoystickCount() do -- ui.header(ac.getJoystickName(i - 1)) -- ui.text(ac.getJoystickAxisCount(i - 1)) -- for j = 1, ac.getJoystickAxisCount(i - 1) do -- ui.text(ac.getJoystickAxisValue(i - 1, j - 1)) -- end -- for j = 1, ac.getJoystickButtonsCount(i - 1) do -- ui.text(ac.isJoystickButtonPressed(i - 1, j - 1) and '1' or '0') -- ui.sameLine(0, 0) -- end -- ui.newLine() -- end -- end