Handling = Handling or {} Handling.Client = Handling.Client or {} -- 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 Handling.Client.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 Handling.Client.SetVehicleHandlingValue(vehicle, class, fieldName, value) local prevValue = fieldName == "nInitialDriveGears" and Handling.Client.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 Handling.Client.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 = Handling.Client.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 = Utils.TableConcat(handling, wizatingHandling) end return handling end ---Calculate new strAdvancedFlags for smooth first gear ---@param advancedFlags integer ---@return integer local function calculateFlagForSmoothFirstGear(advancedFlags) if Utils.HasFlag(advancedFlags, ADV_HANDLING_FLAGS.ELECTRIC) then -- Ignore if the vehicle has an electric gearbox flag return advancedFlags end advancedFlags = Utils.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 Utils.HasFlag(advancedFlags, ADV_HANDLING_FLAGS.ELECTRIC) then -- Ignore if the vehicle has an electric gearbox flag return advancedFlags end if toggle then advancedFlags = Utils.RemoveFlag(advancedFlags, ADV_HANDLING_FLAGS.FULL_AUTO) advancedFlags = Utils.RemoveFlag(advancedFlags, ADV_HANDLING_FLAGS.DIRECT_SHIFT) advancedFlags = Utils.AddFlag(advancedFlags, ADV_HANDLING_FLAGS.MANUAL) else advancedFlags = Utils.RemoveFlag(advancedFlags, ADV_HANDLING_FLAGS.MANUAL) end return advancedFlags end ---Get GTA performance mods for vehicle (this is because we have to reapply them after making handling changes) ---@param vehicle integer ---@return table|false local function getGTAPerformanceMods(vehicle) if not vehicle or vehicle == 0 then return false end return { modEngine = GetVehicleMod(vehicle, 11), modBrakes = GetVehicleMod(vehicle, 12), modTransmission = GetVehicleMod(vehicle, 13), modTurbo = IsToggleModOn(vehicle, 18) } 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 data table local function reapplyGTAPerformanceMods(vehicle, data) if not data or type(data) ~= "table" then return end SetVehicleModKit(vehicle, 0) if data.modEngine then SetVehicleMod(vehicle, 11, data.modEngine, false) end if data.modBrakes then SetVehicleMod(vehicle, 12, data.modBrakes, false) end if data.modTransmission then SetVehicleMod(vehicle, 13, data.modTransmission, false) end if data.modTurbo ~= nil then ToggleVehicleMod(vehicle, 18, data.modTurbo) end end ---@param handling table ---@return table local function copyHandling(handling) local copy = {} for key, value in pairs(handling) do copy[key] = value end return copy 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 = Utils.IsVehicleElectric(GetEntityArchetypeName(vehicle)) -- Suspension local suspensionDamage = Utils.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 = Utils.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 = Utils.Round(servicingHealth.brakePads / 100, 3) handling.fBrakeForce = calcServicingHandlingValue(handling.fBrakeForce, 0.01, brakesDamage) -- Clutch local clutchDamage = Utils.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 = Utils.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 = Utils.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 ---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 if not baseHandling then baseHandling = Handling.Client.GetBaseVehicleHandling(vehicle) Entity(vehicle).state:set("baseHandling", baseHandling, false) end local handling = copyHandling(baseHandling) local performanceMods = getGTAPerformanceMods(vehicle) 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 Handling.Client.SetVehicleHandlingValue(vehicle, HANDLING_KEY_CLASS_MAP[key], key, value) end end if NetworkGetEntityOwner(vehicle) == cache.playerId and performanceMods then reapplyGTAPerformanceMods(vehicle, 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 if not baseHandling then baseHandling = Handling.Client.GetBaseVehicleHandling(vehicle) Entity(vehicle).state:set("baseHandling", baseHandling, false) end local handling = copyHandling(baseHandling) local performanceMods = getGTAPerformanceMods(vehicle) 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 Handling.Client.SetVehicleHandlingValue(vehicle, HANDLING_KEY_CLASS_MAP[key], key, value) end end if NetworkGetEntityOwner(vehicle) == cache.playerId and performanceMods then reapplyGTAPerformanceMods(vehicle, 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 = Handling.Client.GetVehicleHandlingValue(vehicle, "CCarHandlingData", "strAdvancedFlags") --[[@as integer]] Handling.Client.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 = Utils.ParseControlBinding(363), downBind = Utils.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)