428 lines
		
	
	
		
			No EOL
		
	
	
		
			15 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			428 lines
		
	
	
		
			No EOL
		
	
	
		
			15 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
-- 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) |