-- Get subhandling class or just return CHandlingData ---@param vehicle integer local function getVehicleSubhandlingClass(vehicle) local vehicleModel = GetEntityModel(vehicle) local vehicleSubHandlingClass = ( (IsThisModelACar(vehicleModel)) and "CCarHandlingData" or (IsThisModelABike(vehicleModel) or IsThisModelAQuadbike(vehicleModel)) and "CBikeHandlingData" or (IsThisModelABoat(vehicleModel) or IsThisModelAJetski(vehicleModel)) and "CBoatHandlingData" or (IsThisModelAHeli(vehicleModel) or IsThisModelAPlane(vehicleModel)) and "CFlyingHandlingData" or false ) return vehicleSubHandlingClass or "CHandlingData" end ---@param vehicle integer ---@param class string ---@param fieldName string function getVehicleHandlingValue(vehicle, class, fieldName) if string.sub(fieldName, 1, 3) == "vec" then -- is vec return GetVehicleHandlingVector(vehicle, class or "CHandlingData", fieldName) elseif string.sub(fieldName, 1, 1) == "f" then return tonumber(string.format("%.6f", GetVehicleHandlingFloat(vehicle, class or "CHandlingData", fieldName))) else return GetVehicleHandlingInt(vehicle, class or "CHandlingData", fieldName) end end ---@param vehicle integer ---@param class string ---@param fieldName string ---@param value any function setVehicleHandlingValue(vehicle, class, fieldName, value) local prevValue = fieldName == "nInitialDriveGears" and getVehicleHandlingValue(vehicle, class, fieldName) or nil -- Set power to wheels based on selected drivetrain bias if fieldName == "fDriveBiasFront" then local numOfWheels = GetVehicleNumberOfWheels(vehicle) if numOfWheels >= 4 then SetVehicleWheelIsPowered(vehicle, 0, value > 0) -- FWD SetVehicleWheelIsPowered(vehicle, 1, value > 0) -- FWD SetVehicleWheelIsPowered(vehicle, 2, value < 1) -- AWD/RWD SetVehicleWheelIsPowered(vehicle, 3, value < 1) -- AWD/RWD SetVehicleWheelIsPowered(vehicle, 4, value < 1) -- AWD/RWD end end if string.sub(fieldName, 1, 3) == "vec" then -- is vec SetVehicleHandlingVector(vehicle, class or "CHandlingData", fieldName, vector3(value.x, value.y, value.z)) elseif string.sub(fieldName, 1, 1) == "f" then SetVehicleHandlingFloat(vehicle, class or "CHandlingData", fieldName, value + 0.0 --[[@as number]]) else -- is int SetVehicleHandlingInt(vehicle, class or "CHandlingData", fieldName, value --[[@as integer]]) end if fieldName == "nInitialDriveGears" and prevValue ~= value then SetVehicleHighGear(vehicle, value) Citizen.InvokeNative(`SET_VEHICLE_CURRENT_GEAR` & 0xFFFFFFFF, vehicle, value) Citizen.InvokeNative(`SET_VEHICLE_NEXT_GEAR` & 0xFFFFFFFF, vehicle, value) SetTimeout(11, function() Citizen.InvokeNative(`SET_VEHICLE_CURRENT_GEAR` & 0xFFFFFFFF, vehicle, 1) end) end local tsm = GetVehicleTopSpeedModifier(vehicle) ModifyVehicleTopSpeed(vehicle, tsm == -1.0 and 1.0 or tsm) end ---Get vehicle base handling ---@param vehicle integer function getBaseVehicleHandling(vehicle) local subHandlingClass = getVehicleSubhandlingClass(vehicle) local handling = {} for handlingKey, class in pairs(HANDLING_KEY_CLASS_MAP) do if class == "CHandlingData" or class == subHandlingClass then local value = getVehicleHandlingValue(vehicle, class, handlingKey) if handlingKey == "AIHandling" then handling["AIHandling"] = AI_HANDLING_HASH_MAP[value] -- hash lookup for string elseif handlingKey == "handlingName" then handling["handlingName"] = GetDisplayNameFromVehicleModel(GetEntityModel(vehicle)) else handling[handlingKey] = value end end end handling["audioNameHash"] = GetEntityArchetypeName(vehicle) -- Integration with wizating_laptop if GetResourceState("wizating_laptop") == "started" then local wizatingHandling = exports["wizating_laptop"]:getHandlingData(vehicle) or {} handling = tableConcat(handling, wizatingHandling) end return handling end ---Calculate new strAdvancedFlags for smooth first gear ---@param advancedFlags integer ---@return integer local function calculateFlagForSmoothFirstGear(advancedFlags) if hasFlag(advancedFlags, ADV_HANDLING_FLAGS.ELECTRIC) then -- Ignore if the vehicle has an electric gearbox flag return advancedFlags end advancedFlags = addFlag(advancedFlags, ADV_HANDLING_FLAGS.SMOOTH_FIRST_GEAR) return advancedFlags end ---Calculate new strAdvancedFlags for manual gearbox toggle ---@param advancedFlags integer ---@param toggle boolean true = manual / false = auto ---@returns integer advancedFlags local function calculateFlagForManualGearbox(advancedFlags, toggle) if hasFlag(advancedFlags, ADV_HANDLING_FLAGS.ELECTRIC) then -- Ignore if the vehicle has an electric gearbox flag return advancedFlags end if toggle then advancedFlags = removeFlag(advancedFlags, ADV_HANDLING_FLAGS.FULL_AUTO) advancedFlags = removeFlag(advancedFlags, ADV_HANDLING_FLAGS.DIRECT_SHIFT) advancedFlags = addFlag(advancedFlags, ADV_HANDLING_FLAGS.MANUAL) else advancedFlags = removeFlag(advancedFlags, ADV_HANDLING_FLAGS.MANUAL) end return advancedFlags end ---Create new handling table from a tuning config ---@param handling table ---@param tuningConfig { engineSwaps: string, drivetrains: string, turbocharging: string, tyres: string, brakes: string, gearboxes: string } ---@return table newHandling local function calculateTuningHandling(handling, tuningConfig) local tuningsToApply = {} for tune, option in pairs(tuningConfig) do if option then local tuneConfig = Config.Tuning[tune]?[option] if tuneConfig then tuningsToApply[#tuningsToApply + 1] = { order = tuneConfig.handlingApplyOrder or 1, config = tuneConfig } end end end -- Sort the tuning options by their apply order table.sort(tuningsToApply, function(a, b) return a.order < b.order end) for _, tune in ipairs(tuningsToApply) do local tuneConfig = tune.config if tuneConfig then if tuneConfig.manualGearbox then handling.strAdvancedFlags = calculateFlagForManualGearbox(handling.strAdvancedFlags, true) end if tuneConfig.audioNameHash then handling.audioNameHash = tuneConfig.audioNameHash end if tuneConfig.handling then for key, value in pairs(tuneConfig.handling) do if tuneConfig.handlingOverwritesValues then handling[key] = value else handling[key] = (handling[key] or 0) + value end end end end end return handling end ---Calculate the new handling value based on base handling, the min handling value & the current damage ---@param val number ---@param minVal number minimum handling value that wear can allow ---@param damage number between 0 and 1 ---@return number local function calcServicingHandlingValue(val, minVal, damage) return minVal + ((val - minVal) * damage) end ---Calculate new vehicle handling values based on current servicing health ---@param vehicle integer ---@param handling table ---@param servicingHealth table ---@return table newHandling local function calculateServicingHandling(vehicle, handling, servicingHealth) local hash = GetEntityModel(vehicle) local isSupportedVeh = IsThisModelACar(hash) or IsThisModelABike(hash) or IsThisModelAQuadbike(hash) if not isSupportedVeh then return handling end -- You can add new electric vehicles in Config.ElectricVehicles local isElectric = isVehicleElectric(GetEntityArchetypeName(vehicle)) -- Suspension local suspensionDamage = round(servicingHealth.suspension / 100, 3) handling.fCamberStiffnesss = calcServicingHandlingValue(handling.fCamberStiffnesss, 0.0, suspensionDamage) handling.fSuspensionForce = calcServicingHandlingValue(handling.fSuspensionForce, 0.0, suspensionDamage) handling.fAntiRollBarForce = calcServicingHandlingValue(handling.fAntiRollBarForce, 0.0, suspensionDamage) SetVehicleAudioBodyDamageFactor(vehicle, 1.0 - suspensionDamage) -- Tyres local tyresDamage = round(servicingHealth.tyres / 100, 3) handling.fTractionCurveMin = calcServicingHandlingValue(handling.fTractionCurveMin, 0.5, tyresDamage) handling.fTractionCurveMax = calcServicingHandlingValue(handling.fTractionCurveMax, 0.5, tyresDamage) -- Brake Pads local brakesDamage = round(servicingHealth.brakePads / 100, 3) handling.fBrakeForce = calcServicingHandlingValue(handling.fBrakeForce, 0.01, brakesDamage) -- Clutch local clutchDamage = round(servicingHealth.clutch / 100, 3) handling.fClutchChangeRateScaleUpShift = calcServicingHandlingValue(handling.fClutchChangeRateScaleUpShift, 0.0, clutchDamage) handling.fClutchChangeRateScaleDownShift = calcServicingHandlingValue(handling.fClutchChangeRateScaleDownShift, 0.0, clutchDamage) -- Spark Plugs, EV Battery (Affects Acceleration) local accelerationDamage = round((isElectric and (servicingHealth.evBattery or 1) or servicingHealth.sparkPlugs) / 100, 3) handling.fDriveInertia = calcServicingHandlingValue(handling.fDriveInertia, 0.01, accelerationDamage) -- Air Filter, Engine Oil, EV Coolant, EV Motor (Affects Acceleration & Top Speed) local engineDamage = round((isElectric and (math.min(servicingHealth.evCoolant, servicingHealth.evMotor) or 1) or math.min(servicingHealth.airFilter, servicingHealth.engineOil)) / 100, 3) handling.fInitialDriveForce = calcServicingHandlingValue(handling.fInitialDriveForce, 0.1, engineDamage) SetVehicleAudioEngineDamageFactor(vehicle, 1.0 - engineDamage) return handling end ---After making any handling changes, we have to reapply GTA performance mods, ---otherwise vehicles become like 15-20% slower due to ModifyVehicleTopSpeed ---@param vehicle integer ---@param performanceMods table local function reapplyGTAPerformanceMods(vehicle, performanceMods) if not performanceMods or type(performanceMods) ~= "table" then return end local modEngine, modBrakes, modTransmission, modTurbo = performanceMods.modEngine, performanceMods.modBrakes, performanceMods.modTransmission, performanceMods.modTurbo SetVehicleModKit(vehicle, 0) if modEngine then SetVehicleMod(vehicle, 11, modEngine, false) end if modBrakes then SetVehicleMod(vehicle, 12, modBrakes, false) end if modTransmission then SetVehicleMod(vehicle, 13, modTransmission, false) end if modTurbo ~= nil then ToggleVehicleMod(vehicle, 18, modTurbo) end end ---Set tuning handling ---@param vehicle integer local function applyVehicleTuningHandling(vehicle, tuningConfig) if not DoesEntityExist(vehicle) then return error("Vehicle does not exist") end local state = Entity(vehicle).state if not state then return end if state.editorHandlingApplied then return end -- JG Handling overwrite in effect local baseHandling, servicingData = state.baseHandling, state.servicingData local handling = baseHandling if not handling then handling = getBaseVehicleHandling(vehicle) setVehicleStatebag(vehicle, "baseHandling", handling, false) end if tuningConfig then handling = calculateTuningHandling(handling, tuningConfig) end if servicingData then handling = calculateServicingHandling(vehicle, handling, servicingData) end if Config.SmoothFirstGear then handling.strAdvancedFlags = calculateFlagForSmoothFirstGear(handling.strAdvancedFlags) end for key, value in pairs(handling) do if key == "audioNameHash" then ForceUseAudioGameObject(vehicle, value --[[@as string]]) else setVehicleHandlingValue(vehicle, HANDLING_KEY_CLASS_MAP[key], key, value) end end if NetworkGetEntityOwner(vehicle) == cache.playerId then reapplyGTAPerformanceMods(vehicle, state.performanceMods) ToggleVehicleMod(vehicle, 18, tuningConfig.turbocharging == 1) end end AddStateBagChangeHandler("tuningConfig", "", function(bagName, _, value) local vehicle = GetEntityFromStateBagName(bagName) if vehicle == 0 then return end if not value then return end applyVehicleTuningHandling(vehicle, value) end) ---Set servicing handling ---@param vehicle integer local function applyVehicleServicingHandling(vehicle, servicingData) if not DoesEntityExist(vehicle) then return error("Vehicle does not exist") end local state = Entity(vehicle).state if not state then return end if state.editorHandlingApplied then return end -- JG Handling overwrite in effect local baseHandling, tuningConfig = state.baseHandling, state.tuningConfig local handling = baseHandling if not handling then handling = getBaseVehicleHandling(vehicle) setVehicleStatebag(vehicle, "baseHandling", handling, false) end if tuningConfig then handling = calculateTuningHandling(handling, tuningConfig) end if servicingData then handling = calculateServicingHandling(vehicle, handling, servicingData) end if Config.SmoothFirstGear then handling.strAdvancedFlags = calculateFlagForSmoothFirstGear(handling.strAdvancedFlags) end for key, value in pairs(handling) do if key ~= "audioNameHash" then setVehicleHandlingValue(vehicle, HANDLING_KEY_CLASS_MAP[key], key, value) end end if NetworkGetEntityOwner(vehicle) == cache.playerId then reapplyGTAPerformanceMods(vehicle, state.performanceMods) end end AddStateBagChangeHandler("servicingData", "", function(bagName, _, value) local vehicle = GetEntityFromStateBagName(bagName) if vehicle == 0 then return end if not value then return end applyVehicleServicingHandling(vehicle, value) end) local function onEnterVehicle(vehicle) if not vehicle or vehicle == 0 then return end -- If Config.SmoothFirstGear is enabled if Config.SmoothFirstGear then local advancedFlags = getVehicleHandlingValue(vehicle, "CCarHandlingData", "strAdvancedFlags") --[[@as integer]] setVehicleHandlingValue(vehicle, "CCarHandlingData", "strAdvancedFlags", calculateFlagForSmoothFirstGear(advancedFlags)) end local state = Entity(vehicle).state or {} if state.tuningConfig then applyVehicleTuningHandling(vehicle, state.tuningConfig) end if state.servicingData then applyVehicleServicingHandling(vehicle, state.servicingData) end end -- Notify user with gear change key binds if they stay at high RPM for an extended period CreateThread(function() local wait = 5000 local timeAtHighRpm = 0 if not Config.ManualHighRPMNotifications then return end while true do if cache.vehicle then local hasManualGearbox = Entity(cache.vehicle).state.tuningConfig?.gearboxes == 1 if not hasManualGearbox then wait = 5000 goto continue end local rpm = GetVehicleCurrentRpm(cache.vehicle) if rpm < 0.99 then timeAtHighRpm = 0 wait = 2000 goto continue end local currentGear = GetVehicleCurrentGear(cache.vehicle) local highGear = GetVehicleHighGear(cache.vehicle) if currentGear < 1 or currentGear == highGear then timeAtHighRpm = 0 wait = 2000 goto continue end wait = 100 timeAtHighRpm += 100 if timeAtHighRpm >= 2000 then SendNUIMessage({ type = "manual-gearbox-keybinds", upBind = parseControlBinding(363), downBind = parseControlBinding(364), locale = Locale, config = Config, }) timeAtHighRpm = 0 end end ::continue:: Wait(wait) end end) lib.onCache("vehicle", onEnterVehicle) if cache.vehicle then onEnterVehicle(cache.vehicle) end -- Exports exports("calculateTuningHandling", calculateTuningHandling) exports("calculateServicingHandling", calculateServicingHandling) exports("applyVehicleTuningHandling", applyVehicleTuningHandling)