--[[ Some helper utils for creating custom automatic gearboxes and such. For example, to switch regular clutch to torque converter, use: ```lua local automaticGearboxUtils = require('shared/physics/automatic-gearbox-utils') automaticGearboxUtils.smartTorqueConverter() ``` Feel free to copy code from this library to your script if you’d want to change anything. An example of extended use of these functions is available at: https://github.com/ac-custom-shaders-patch/acc-lua-examples/blob/main/cars_physics/gearboxes/script_at.lua https://github.com/ac-custom-shaders-patch/acc-lua-examples/blob/main/cars_physics/gearboxes/script_amt.lua ]] ---@diagnostic disable local automaticGearboxUtils = {} ---Last ratio of root to engine velocity as seen by the custom clutch implementation from the last frame. ---If you’re creating a custom torque converter or something like that, please make sure to update this value. automaticGearboxUtils.lastRatio = 0 ---Brake with gearbox. Actual implementation might change in the future. ---@param breakingThresholdKmh number @Gearbox will get damaged if wheels are spinning fastar than this threshold. function automaticGearboxUtils.parkingBrake(breakingThresholdKmh) -- Braking with gearbox ac.overrideSpecificValue(ac.CarPhysicsValueID.DrivetrainDriveVelocityMult, 0) -- Break clutch if wheels are spinning local carPh = ac.accessCarPhysics() local maxSpeed = math.max(math.abs(carPh.wheels[2].angularSpeed), math.abs(carPh.wheels[3].angularSpeed)) * car.wheels[2].tyreRadius if maxSpeed * 3.6 > (breakingThresholdKmh or 5) then ac.setGearsGrinding(true, 1) end end ---@alias AutomaticLogicFn fun(currentGear: integer): integer?, number? @A callback taking current gear and returning a desired gear together with how long this desired gear should be desired for (in seconds) before shift is allowed (return 0 as a second argument to shift instantly, or `nil` to not shift at all). ---Simple helper for automatic transmission logic. ---@param logicFn AutomaticLogicFn ---@param totalShiftTime number @Time to shift between gears, in seconds ---@param minGear integer @Minimum possible gear. ---@param maxGear integer @Maximum possible gear. ---@param maxShiftGap integer? @How many steps can be made at once. Default value: 1. ---@return fun(currentGear: integer, dt: number): integer, integer, number @Function takes current gear index and returns updated current gear index, change progress from previous to current and previous gear index. function automaticGearboxUtils.logicHelper(logicFn, totalShiftTime, minGear, maxGear, maxShiftGap) minGear = minGear or 2 maxGear = maxGear or 5 maxShiftGap = maxShiftGap or 1 local prevGearIndex = 1 local gearShiftProgress = 1 local dShiftCandidateIndex = -1 local dShiftCandidateTimer = 0 local dtMult = 1 / totalShiftTime return function(currentGear, dt) if gearShiftProgress < 1 then gearShiftProgress = gearShiftProgress + dt * dtMult else local betterGearIndex, requiredTime = logicFn(currentGear) betterGearIndex = math.clamp(betterGearIndex or currentGear, minGear, maxGear) betterGearIndex = math.clamp(betterGearIndex, currentGear - maxShiftGap, currentGear + maxShiftGap) requiredTime = requiredTime or math.huge if betterGearIndex == currentGear then dShiftCandidateIndex, dShiftCandidateTimer = -1, 0 elseif dShiftCandidateIndex ~= betterGearIndex then dShiftCandidateIndex, dShiftCandidateTimer = betterGearIndex, 0 elseif dShiftCandidateTimer > requiredTime then prevGearIndex, currentGear, gearShiftProgress = currentGear, betterGearIndex, 0 else dShiftCandidateTimer = dShiftCandidateTimer + dt end end return currentGear, gearShiftProgress, prevGearIndex end end ---Creates a very simple drive logic function changing shifting thresholds based on smoothed gas input. Base thresholds ---are taken from automatic shifter settings from “drivetrain.ini”. ---@param shiftUpDelay number? @Delay for upshifts, in seconds. Default value: 0.5. ---@param shiftDownDelay number? @Delay for downshifts, in seconds. Default value: 0.05. ---@return AutomaticLogicFn function automaticGearboxUtils.simpleDriveFnFactory(shiftUpDelay, shiftDownDelay) shiftUpDelay = shiftUpDelay or 0.5 shiftDownDelay = shiftDownDelay or 0.05 local carPh = ac.accessCarPhysics() local cfgAI = ac.INIConfig.carData(car.index, 'ai.ini') local gearDownFast = cfgAI:get('GEARS', 'DOWN', car.rpmLimiter * 0.6) local gearDownSlow = math.lerp(car.rpmMinimum, gearDownFast, 0.5) local gearBand = cfgAI:get('GEARS', 'UP', car.rpmLimiter) - gearDownFast local smoothGas = 0 return function(curGearIndex) smoothGas = math.lerp(smoothGas, carPh.gas, 0.005) if carPh.rpm > math.lerp(gearDownSlow, gearDownFast, smoothGas) + gearBand then return curGearIndex + 1, shiftUpDelay elseif carPh.rpm < math.lerp(gearDownSlow, gearDownFast, smoothGas) then return curGearIndex - 1, shiftDownDelay end end end ---A better algorithm trying to recreate a mechanical automatic gearbox approximation made by Dmitry A. ---@return AutomaticLogicFn function automaticGearboxUtils.oilDriveFnFactory() local carPh = ac.accessCarPhysics() local minimumRPM = car.rpmMinimum local limiterRPM = car.rpmLimiter local gearMult = 0 local prevGearIndex return function(curGearIndex) if curGearIndex ~= prevGearIndex then prevGearIndex = curGearIndex gearMult = 0 end local rpm = carPh.rpm gearMult = gearMult * 0.995 gearMult = gearMult + (math.pow(rpm / (math.max(minimumRPM + 1000, math.pow(carPh.gas, 2) * (limiterRPM * 0.7))), 2) - 1.1) * 0.02 gearMult = rpm > (limiterRPM - 1000) and 1 or gearMult gearMult = rpm < (minimumRPM + 1000) and -1 or gearMult gearMult = math.clamp(gearMult, -1, 1) if rpm < (limiterRPM / 2) and carPh.gas > 0.99 or rpm < (minimumRPM + 100) then gearMult = -2 end local gmF = math.floor(math.abs(gearMult)) * math.sign(gearMult) if rpm < (limiterRPM - 1000) and carPh.gas > 0.99 then gmF = math.min(gmF, 0) end return curGearIndex + gmF, 0.3 end end ---Set up a very simple torque converter which uses one of those torque converter graphs with ---torque ratio and K-factor you can find lying around. Curves are loaded from `drivetrain.ini`. --- ---An example of a config: ---```ini ---[SCRIPT_TORQUE_CONVERTER] ---K_FACTOR=(|0=146|0.2=145|0.4=144|0.6=145|0.7=152|0.75=157|0.8=165|0.85=173|0.9=185|0.92=200|0.94=220|0.96=255|0.975=320|0.98=500|1=10000|) ---TR_CURVE=(|0=2|0.2=1.8|0.4=1.61|0.6=1.38|0.8=1.13|0.9=1.01|1=1|) ---``` --- ---When engine is going slower than gearbox (coasting), it acts as a simple hydralic coupling, ---because I couldn’t figure out what else should it do. function automaticGearboxUtils.simpleTorqueConverter() -- Pro tip: add “SCRIPT_”, or even something -- more unique, to your section names to ensure there won’t be a conflict with future CSP updates and such. Or maybe -- even use custom file names. local cfgDrivetrain = ac.INIConfig.carData(car.index, 'drivetrain.ini') local curveKFactor = cfgDrivetrain:tryGetLut('SCRIPT_TORQUE_CONVERTER', 'K_FACTOR') or error('SCRIPT_TORQUE_CONVERTER/K_FACTOR is not set') local curveTR = cfgDrivetrain:tryGetLut('SCRIPT_TORQUE_CONVERTER', 'TR_CURVE') or error('SCRIPT_TORQUE_CONVERTER/TR_CURVE is not set') local function torqueConverter(engineVelocity, rootVelocity) automaticGearboxUtils.lastRatio = rootVelocity / engineVelocity if engineVelocity < 0.01 then return 0, 0 end if engineVelocity > rootVelocity then -- Engine is faster than gearbox, using torque converter model with all of its fan stuff local engineRPM = engineVelocity * (30 / math.pi) local ratio = math.min(1, rootVelocity / engineVelocity) local engineTorque = math.pow(engineRPM / curveKFactor:get(ratio), 2) return -engineTorque, engineTorque * curveTR:get(ratio) else -- Engine is slower, fan is backwards, so let’s say it’s just a hydralic coupling local speedDelta = math.pow(rootVelocity - engineVelocity, 2) * 0.1 return speedDelta, -speedDelta end end ac.replaceClutch(function(engineVelocity, rootVelocity, engineInertia, rootInertia, clutchAmount, dt) return torqueConverter(engineVelocity, rootVelocity) end) end ---Alternative torque converter model by Dmitry A. Smoother and nicer. function automaticGearboxUtils.smartTorqueConverter() ac.replaceClutch(function(engineVelocity, rootVelocity, engineInertia, rootInertia, clutchAmount, dt) automaticGearboxUtils.lastRatio = rootVelocity / engineVelocity local srMult = 1 local SR = 1 + srMult - (math.clamp(rootVelocity, 0, engineVelocity) / math.max(engineVelocity, 1)) * srMult local tcDiff = (engineVelocity - rootVelocity) / SR local tcTorq = math.clamp(tcDiff * math.pow(engineVelocity * 0.01, 1.5), -1000, 1000) return -tcTorq, tcTorq * SR end) end return automaticGearboxUtils