Main/resources/[jobs]/[mechanic]/jg-mechanic/client/cl-handling.lua
2025-06-07 08:51:21 +02:00

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)