Configuration
Detailed configuration information
Last updated
Was this helpful?
Detailed configuration information
Last updated
Was this helpful?
All config files fields are explained inside the config file in commented code. Some more complicated fields are explained in this page.
You can create custom modifications in the config files that will affect handling fields or assign variables to vehicles when installed.
By default custom mods are installed by using an inventory item. If you'd like your own implementation, you could use or edit server_config.lua Items
section.
You can find custom mods data in config.luaunder Custom Mods
section.
uniqueId: string
Required id to be used to identify each mod. Must be unique for each mod.
itemData: table
Contains data for inventory item.
consume: boolean
Wether or not the item should be removed from inventory after installing the mod.
name: ?string
Item name (not label) that is used to identify your inventory item. If not entered will default to uniqueId
. Make sure to create the items in your database / items config for your inventory for these to work.
label: string
Label that will be shown in tablet. If not entered will default to identifier
.
identifier: string
Same as slotName
. This will identify what slot the part will be installed to. Meaning that if two different mods will be created with the same identifier
, only one will be available to be installed at a time.
save: boolean
Wether or not to save the part in database. If the part would not be saved in database, it will disappear after respawning the vehicle.
installedByDefault: boolean
If set to true
the part will be installed by default to all vehicles. Might be useful for standalone servers that do not have any inventory item system.
handlingData: table
Data that will be assigned to the vehicle.
If key
matches any valid handling field it will affect the vehicle's handling field e.g. fInitialDriveForce = {value = 0.01}
will increase the fInitialdriveForce
by 0.01
.
If key is equal to enableHandlingFields
it will make the fields available for adjusting in tuning tablet.
Othewise it will assign the variable to vehicle for any other desired implementation.
enableHandlingFields: string[]
Make the fields available for adjusting in tuning tablet
['fTractionCurveMax' | 'fInitialDriveForce' | 'fInitialDriveMaxFlatVel' | <...>
]: {value: ?number, mult: ?number}
Affects the handling value by value
or adds the multiplier (mult
) when the mod is installed.
[string
]: any
Assigns the variable to vehicle.
Config.CustomMods = {
-- ...other mods
{
uniqueId = 'car_mod_engine_ecu',
itemData = ,
label = 'ECU',
identifier = ,
save = true,
handlingData = {
enableHandlingFields =
{
},
,
},
},
-- ...other mods
}
Permissions will let you set up what is allowed to each group or player. Currently there are a few permissions that will allow or block players from doing certain things with the script.
You can find custom mods data in config.luaunder Permissions
section.
readXml
Allows player to preview XML file output in UI.
writeXml
Allows player to write XML file data to your server files.
writeDefault
Allows player to write default handlings to server database.
tabletCommand
Allows player to use chat command to open tablet. Recommended for servers that do not have inventory system.
driftCommand
Allows player to use command to enable drift mode. This will not require for player to have a small tablet. Recommended for servers that do not have inventory system.
ignoreLimits
Allows player to change values without strict limits. This is useful if player has to adjust handlings to write them to server files.
Defines what groups have which permissions. You can find the implementation of permissions in configs/server_config.lua
or adjust the getPlayerPemissions
function to your liking.
type: 'frameworkgroup' | 'identifier' | 'ace'
'frameworkgroup'
- selects the group that is in value
.
'identifier'
- selects specific player by identifier. Identifier is entered in value
.
'ace'
- selects ace permission specified in value
.
value: string
permissions: Permissions
Defines which permissions are assigned to the selected player or group.
SB = {}
SB.FrameworkType = ''
SB.FrameworkObject = nil
local function fetchSharedObject(resourceName, exportName, eventName)
try(function()
SB.FrameworkObject = exports[resourceName][exportName]()
TriggerEvent('sb-handlingtuning:frameworkLoaded', SB.FrameworkType)
end, function(err)
print(err)
TriggerEvent(eventName, function(Obj)
SB.FrameworkObject = Obj
end)
Citizen.CreateThread(function()
Citizen.Wait(500)
if not SB.FrameworkObject then
DebugPrint('^9ERROR^7', -1, 'Framework loading failed. Given parameters:')
DebugPrint('^9ERROR^7', -1,
([[
Framework name: ^4%s^7,
event name: ^4%s^7,
export function name: ^4%s^7,
used type: ^4%s^7
]]):format(
Config.Framework, eventName, exportName, SB.FrameworkType))
DebugPrint('NOTE', -1,'If any of the parameters do not match your server settings please check your ^4client_config.lua^7 files')
SB.FrameworkType = 'standalone'
end
TriggerEvent('sb-handlingtuning:frameworkLoaded', SB.FrameworkType)
end)
end)
end
Citizen.CreateThread(function()
if Config.Framework == 'auto' then
if GetResourceState('es_extended') == 'started' then
SB.FrameworkType = 'esx'
elseif GetResourceState('qb-core') == 'started' then
SB.FrameworkType = 'qbcore'
else
SB.FrameworkType = 'standalone'
end
elseif Config.Framework == 'esx' then
SB.FrameworkType = 'esx'
elseif Config.Framework == 'qbcore' then
SB.FrameworkType = 'qbcore'
elseif Config.Framework == 'standalone' then
SB.FrameworkType = 'standalone'
else
SB.FrameworkType = 'custom'
end
local evName = IsDuplicityVersion() and Config.FrameworkData.SharedObjectEventNameSV or
Config.FrameworkData.SharedObjectEventNameCL
if SB.FrameworkType == 'esx' then
emitNetCB(SVCB.FETCH_FRAMEWORK_RESOURCE_NAME, function(resName)
fetchSharedObject(resName, 'getSharedObject', evName or 'esx:getSharedObject')
end)
elseif SB.FrameworkType == 'qbcore' then
emitNetCB(SVCB.FETCH_FRAMEWORK_RESOURCE_NAME, function(resName)
if GetResourceState('qbx_core') == 'started' then
fetchSharedObject('qb-core', 'GetCoreObject', evName or 'QBCore:GetObject')
else
fetchSharedObject(resName, 'GetCoreObject', evName or 'QBCore:GetObject')
end
end)
elseif SB.FrameworkType == 'standalone' then
-- Add code for standalone if needed
elseif SB.FrameworkType == 'custom' then
-- Add code for your custom framework
DebugPrint('^6ERROR^7', -1,
'Framework loading failed. No code for custom framework provided, check your ^4*/framework.lua^7 files')
end
DebugPrint('FRAMEWORK', -1,
([[
Framework name: ^4%s^7,
event name: ^4%s^7,
export function name: ^4%s^7,
used type: ^4%s^7
]]):format(
Config.Framework, eventName, exportName, SB.FrameworkType))
end)
---Selects notification type and sends notification
---@param msg string
function Notify(msg)
if Config.Notifications == 'framework' then
if SB.FrameworkType == 'qbcore' or SB.Framework == 'qb' then
SB.FrameworkObject.Functions.Notify(msg)
elseif SB.FrameworkType == 'esx' then
SB.FrameworkObject.ShowNotification(msg)
end
elseif Config.Notifications == 'chat' then
TriggerEvent('chat:addMessage', {
color = {255, 0, 0},
multiline = true,
args = {'[HandlingTuning]', msg}
})
end
end
RegisterNetEvent('handlingtuning:Notify', function(...)
Notify(...)
end)
---Checks if vehicled has access to drift mode
---@param veh number
---@return boolean | 'lsd' | 'welded' | 'open'
function IsDriftModeAvailable(veh)
if not Config.DriftModeEnabled then
return false
end
---@type 'lsd' | 'welded' | 'open'
local vehicleDifferentialMod = getVehicleDifferentialMod(veh)
if vehicleDifferentialMod == 'open' then
return false
else
return vehicleDifferentialMod
end
end
---Get license trimmed plate
function GetVehicleLicensePlate(vehicle)
return string.trim(GetVehicleNumberPlateText(vehicle))
end
---Checks if vehicled has access to drift mode
---@param veh number
function IsDriftModeConfigurationAvailable(veh)
return IsDriftModeAvailable(veh) == 'lsd'
end
function ErrorPrint(...)
local text = table.concat({ ... }, ' ')
local string = ('[^6ERROR^7] - %s ^7'):format(text)
print(string)
if not CFG or CFG.SendClientSideErrorsToServer then return end
TriggerServerEvent('sb-interaction:errorPrint', ('[^4Client^7] %s ^7'):format(string))
end
Config = {}
---@type 'kmh' | 'mph' Speed unit used in tablet and handling data
Config.SpeedUnit = 'kmh'
---@type number Speed unit multiplier. 3.6 for kmh / 2.236936 for mph
Config.SpeedMultiplier = 3.6
---@type number How often the telemetry page in small tablet data is refreshed. iIn ms
Config.StatsRefreshTimeout = 200
---@type number Base Nitro Consumption rate. Liters per second
Config.BaseNitroConsumption = 0.1
---@type number Base Nitro fInitialDriveForce value.
Config.BaseNitroDriveForceMultiplier = 0.7
---@type number Base Nitro fInitialDragCoeff multiplier.
Config.BaseNitroDragCoefficientMultiplier = -0.5
-- 8888888888 888
-- 888 888
-- 888 888
-- 8888888 888d888 8888b. 88888b.d88b. .d88b. 888 888 888 .d88b. 888d888 888 888
-- 888 888P" "88b 888 "888 "88b d8P Y8b 888 888 888 d88""88b 888P" 888 .88P
-- 888 888 .d888888 888 888 888 88888888 888 888 888 888 888 888 888888K
-- 888 888 888 888 888 888 888 Y8b. Y88b 888 d88P Y88..88P 888 888 "88b
-- 888 888 "Y888888 888 888 888 "Y8888 "Y8888888P" "Y88P" 888 888 888
---@type 'auto' | 'esx' | 'qbcore' | 'custom' If you are using custom framework, set this to 'custom' and configure the framework in client_config.lua and server_config.lua
Config.Framework = 'auto'
Config.FrameworkData = { -- ONLY change these if you have custom names set on your framework
SharedObjectEventNameCL = nil, -- Framework shared object event name for client side
SharedObjectEventNameSV = nil, -- Framework shared object event name for server side
}
---@type 'framework' | 'chat' | 'custom' Notification system used for displaying messages. Can be configured in client_config.lua
Config.Notifications = 'framework'
---@type 'auto' | 'oxmysql' | 'mysql-async' | 'ghmattimysql' | 'NO'
Config.MySQLScript = 'auto'
---@type 'license' | 'steam' | 'discord' | 'ip' Identifier used to save/load player's presets in database
Config.PlayerIdentifier = 'license'
-- Recommended to keep at true. Only use false if you are not using any database.
Config.UseDatabaseHandlingSaving = true
-- Recommended to keep at true. Only use false if you are not using any database.
Config.UseDatabasePresetsSaving = true
-- Recommended to keep at true. Only use false if you are not using any database.
Config.UseDatabaseDefaultHandlingSaving = true
-- Recommended to keep at false. Only use true if you are not using any database.
Config.SaveDefaultsToFile = false
-- If true, will run database checks on server start. Recommended to keep at true.
Config.RunDatabaseChecks = true
-- If true, will automatically save missing default values to database when the vehicle handling is required.
-- WARNING: Recommended to keep at false and save all default values manually by using /admin:cars
Config.AutomaticallySaveMissingDefaultValues = false
AddEventHandler('sb-handlingtuning:frameworkLoaded', function(framework)
if table.has({ 'standalone', 'custom' }, framework) then
Config.SaveDefaultsToFile = true
end
if IsDuplicityVersion() then
-- Owned vehicles table in database information.
-- tableName - Name of table in which all owned vehicles are stored
-- ownerColumn - Owned vehicles owner column in DB table
-- plateColumn - Vehicle plate column in DB table
if SB.FrameworkType == 'qbcore' then
Config.OwnedVehiclesDataTable = {
tableName = 'player_vehicles', ownerColumn = 'citizenid', plateColumn = 'plate' -- QBCore
}
elseif SB.FrameworkType == 'esx' then
Config.OwnedVehiclesDataTable = {
tableName = 'owned_vehicles', ownerColumn = 'owner', plateColumn = 'plate' -- ESX
}
else
Config.OwnedVehiclesDataTable = {}
Config.UseDatabaseHandlingSaving = false
Config.UseDatabasePresetsSaving = false
end
DB_INFO = Config.OwnedVehiclesDataTable
end
end)
if IsDuplicityVersion() then
AddEventHandler('sb-handlingtuning:databaseScriptDetected', function(databaseScript)
if databaseScript == 'NO' then
Config.UseDatabaseHandlingSaving = false
Config.UseDatabasePresetsSaving = false
Config.SaveDefaultsToFile = true
Config.RunDatabaseChecks = false
end
end)
end
-- d8888
-- d88888
-- d88P888
-- d88P 888 .d8888b .d8888b .d88b. .d8888b .d8888b
-- d88P 888 d88P" d88P" d8P Y8b 88K 88K
-- d88P 888 888 888 88888888 "Y8888b. "Y8888b.
-- d8888888888 Y88b. Y88b. Y8b. X88 X88
-- d88P 888 "Y8888P "Y8888P "Y8888 88888P' 88888P'
-- Limited editing allows for all players to use the tablet, but only adjust vehicles handling values for limited number
-- You can adjust how much of each field can be changed by editing configs/config.js Min / max > changeLimit
--
-- For example if vehicle's standard mass is 2000, Min.changeLimit is 200 and max.changeLimit is 500,
-- then player will have 1800 - 2500 to play around with, as 2000 - 200 = 1800, 2000 + 500 = 2500.
-- If value in config.js is not added, this will be used instead
Config.DefaultChangeLimit = 0.1
---@class Permissions
---@field readXml ?boolean allows to view and copy xml file in UI
---@field writeXml ?boolean allows to write xml data to server handling.meta
---@field writeDefault ?boolean allows to write default handlings data to database / file
---@field tabletCommand ?boolean allows to open tablet without having the inventory item
---@field driftCommand ?boolean allows to use drift mode without having the inventory item or required mods on car
---@field ignoreLimits ?boolean allows to ignore change limits
---@type Permissions Defines what permissions are given to each player by default
Config.DefaultPermissions = {
readXml = false,
writeXml = false,
writeDefault = false,
tabletCommand = false,
driftCommand = false,
ignoreLimits = false,
}
--- DO NOT EDIT THE FULL_PERMS
---@type Permissions
FULL_PERMS = {
readXml = true,
writeXml = true,
writeDefault = true,
tabletCommand = true,
driftCommand = true,
ignoreLimits = true,
}
--- Permissions and their conditions
--- type: 'identifier' | 'frameworkgroup' | 'ace' | 'default'
--- value: data for type e.g. framework group name, ace permission name, identifier value
--- permissions: Permissions that are assigned to selector
--- You can see `getPlayerPemissions` function in server_config.lua for implementation of permissions
---@type {type: string, value?: string, permissions: Permissions}[]
Config.Permissions = {
{ type = 'frameworkgroup', value = 'god', permissions = FULL_PERMS },
{ type = 'frameworkgroup', value = 'superadmin', permissions = FULL_PERMS },
{ type = 'frameworkgroup', value = 'admin', permissions = FULL_PERMS },
-- {type = 'identifier', value = 'steam:abc123', permissions = {}},
-- {type = 'ace', value = 'handlingtuning', permissions = {}},
}
-- Name of inventory item
-- Follor the documentation on how to add new items:
-- https://squirrel-bracket.gitbook.io/squirrel-bracket-documentation/v/handling-tuning/installation#create-inventory-items
Config.TabletItemName = 'tunertablet'
Config.SmallTabletItemName = 'smalltunertablet'
-- Command for admins full tablet access
Config.TabletCommand = 'tablet'
Config.SmallTabletCommand = 'smalltablet'
-- These fields will be disabled by default, in order to enable them, you will need to add mods to the car that enable them.
-- For example, if you add 'fSteeringLock' in Config.DisabledFields it will not be shown in tablet, unless you add a mod that enables it and install it on the car:
-- {
-- itemData = {name = 'car_mod_lock_kit', consume = true},
-- identifier = 'lockKit',
-- save = true,
-- handlingData = {enableHandlingFields = {'fSteeringLock'}}
-- }
Config.DisabledFields = {
'nMonetaryValue',
'fPetrolTankVolume',
'fSteeringLock',
'fInitialDriveForce',
'fDownforceModifier',
'fPetrolTankVolume',
'fDriveInertia',
'fRollCentreHeightRear',
'fDriveBiasFront',
'fPercentSubmerged',
'fEngineDamageMult',
'fLowSpeedTractionLossMult',
'fWeaponDamageScaledToVehHealthMult',
'fOilVolume',
'fSeatOffsetDistX',
'fTractionCurveLateral',
'fSuspensionForce',
'fMass',
'fSeatOffsetDistY',
'fSeatOffsetDistZ',
'fTractionSpringDeltaMax',
'nInitialDriveGears',
'fBoostMaxSpeed',
'fCollisionDamageMult',
'fWeaponDamageMult',
'vecInertiaMultiplier',
'vecCentreOfMassOffset',
'fSuspensionCompDamp',
'fRocketBoostCapacity',
}
-- 8888888b. d8b .d888 888 888b d888 888
-- 888 "Y88b Y8P d88P" 888 8888b d8888 888
-- 888 888 888 888 88888b.d88888 888
-- 888 888 888d888 888 888888 888888 888Y88888P888 .d88b. .d88888 .d88b.
-- 888 888 888P" 888 888 888 888 Y888P 888 d88""88b d88" 888 d8P Y8b
-- 888 888 888 888 888 888 888 Y8P 888 888 888 888 888 88888888
-- 888 .d88P 888 888 888 Y88b. 888 " 888 Y88..88P Y88b 888 Y8b.
-- 8888888P" 888 888 888 "Y888 888 888 "Y88P" "Y88888 "Y8888
-- Master switch for drift mode
Config.DriftModeEnabled = true
-- If false the drift mode won't be able to be toggled while moving
Config.AllowMovingDriftToggle = false -- If false the drift mode won't be able to be toggled while moving
---@alias DriftFieldFormula fun(defaultValue: number, speed: number, angle: number, multiplier: number): number
---@class DriftModeField
---@field label string what is displayed in tablet under the Drift group configuration
---@field description string what is displayed in tablet under the Drift group configuration
---@field formula DriftFieldFormula formula used to calculate changes for the field when vehicle is moving sideways
---@field defaultValue ?number
---Defines data for drift mode
---@type table<string, DriftModeField>
Config.DriftModeFields = {
fInitialDriveForce = {
label = 'Drive Force multiplier on slide',
description = 'How much of drive force will be increased when vehicle slides',
formula = function(defaultValue, speed, angle, multiplier)
local result = defaultValue + ((angle / 15) * multiplier)
return result
end,
min = 0.0,
max = 1.0,
defaultValue = 0.5,
},
fSteeringLock = {
label = 'Steering lock multiplier on slide',
formula = function(defaultValue, speed, angle, multiplier)
local result = math.max(defaultValue, math.min(defaultValue + (angle * multiplier), 70.0))
return result
end,
min = 0.0,
max = 1.0,
defaultValue = 0.5,
},
fTractionCurveMin = {
label = 'Traction decrease multiplier on slide',
formula = function(defaultValue, speed, angle, multiplier)
local result = math.max(defaultValue - angle / (17 * multiplier), 1.0)
return result
end,
min = 0.0,
max = 1.0,
defaultValue = 0.5,
},
fDriveBiasFront = {
label = 'Drive bias multiplier on slide',
formula = function(defaultValue, speed, angle, multiplier)
local result = math.max(defaultValue, 0.2)
return result
end,
min = 0.0,
max = 1.0,
defaultValue = 0.5,
},
}
-- .d88888b. 888 888b d888 888
-- d88P Y88b 888 8888b d8888 888
-- 888 888 888 88888b.d88888 888
-- 888 888 888 .d8888b 888888 .d88b. 88888b.d88b. 888Y88888P888 .d88b. .d88888 .d8888b
-- 888 888 888 88K 888 d88""88b 888 "888 "88b 888 Y888P 888 d88""88b d88" 888 88K
-- 888 888 888 888 "Y8888b. 888 888 888 888 888 888 888 Y8P 888 888 888 888 888 "Y8888b.
-- Y88b d88P Y88b 888 X88 Y88b. Y88..88P 888 888 888 888 " 888 Y88..88P Y88b 888 X88
-- "Y8888P" "Y88888 88888P' "Y888 "Y88P" 888 888 888 888 888 "Y88P" "Y88888 88888P'
-- You can add more. Read more on docs:
-- https://squirrel-bracket.gitbook.io/squirrel-bracket-documentation/v/handling-tuning/configuration#custom-mods
-- Read docs on how to add inventory items:
-- https://squirrel-bracket.gitbook.io/squirrel-bracket-documentation/v/handling-tuning/installation#create-inventory-items
Config.CustomMods = {
-- Default custom mods required for drift mode and nitro:
{
uniqueId = 'car_mod_nitro_kit',
itemData = { consume = true },
label = 'Nitro Kit',
identifier = 'nitroKit',
save = true,
installedByDefault = false, -- makes the mod to be installed on all vehicles by default
handlingData = {}, -- leave it empty
},
{
uniqueId = 'car_mod_nitro_tank',
itemData = { consume = true },
label = 'Nitro Tank',
identifier = 'nitroTank',
save = true,
allowReapply = true,
installedByDefault = false,
handlingData = { size = 3.0, level = 3.0 },
},
{
uniqueId = 'car_mod_diff_lsd',
itemData = { consume = true },
label = 'LSD',
identifier = 'differential',
save = true,
handlingData = { type = 'lsd' },
},
{
uniqueId = 'car_mod_diff_welded',
itemData = { consume = true },
label = 'Welded Differential',
identifier = 'differential',
save = true,
handlingData = { type = 'welded' },
},
-- Additional custom mods:
{
uniqueId = 'car_mod_tires',
itemData = { consume = true },
label = 'Sport tires',
identifier = 'tires',
save = true,
handlingData = { fTractionCurveMax = { value = 0.3 }, fTractionCurveMin = { value = 0.3 } },
},
{
uniqueId = 'car_mod_stiff_arb',
itemData = { consume = true },
label = 'Stiff ARB',
identifier = 'arb',
save = true,
handlingData = { fAntiRollBarForce = { value = 0.2 } },
},
{
uniqueId = 'car_mod_ecu',
itemData = { consume = false },
label = 'ECU',
identifier = 'engine_ecu',
save = true,
handlingData = { enableHandlingFields = { 'fInitialDriveForce', 'fInitialDriveMaxFlatVel' }, fInitialDriveForce = { value = 0.01 }, fInitialDriveMaxFlatVel = { value = -2 } },
},
{
uniqueId = 'car_mod_lock_kit',
itemData = { consume = true },
label = 'Steer lock kit',
identifier = 'lockKit',
save = true,
handlingData = { enableHandlingFields = { 'fSteeringLock' } }
}
}
-- 8888888888 d8b .d8888b.
-- 888 Y8P d88P Y88b
-- 888 Y88b.
-- 8888888 88888b. .d88b. 888 88888b. .d88b. "Y888b. 888 888 888 8888b. 88888b.
-- 888 888 "88b d88P"88b 888 888 "88b d8P Y8b "Y88b. 888 888 888 "88b 888 "88b
-- 888 888 888 888 888 888 888 888 88888888 "888 888 888 888 .d888888 888 888
-- 888 888 888 Y88b 888 888 888 888 Y8b. Y88b d88P Y88b 888 d88P 888 888 888 d88P
-- 8888888888 888 888 "Y88888 888 888 888 "Y8888 "Y8888P" "Y8888888P" "Y888888 88888P"
-- 888 888
-- Y8b d88P 888
-- "Y88P" 888
-- Implementation of engine swap feature can be edited in **/*/engine_swap.lua
Config.EngineSwap = {
-- Master switch for engine swap feature
enabled = true,
-- Time it takes to install engine
time = 20000,
-- TODO: Add sound transfer
-- transferSound = false,
-- Engine swap values that will be transferred from donor engine to target vehicle
transferValues = {
'fInitialDragCoeff',
'nInitialDriveGears',
'fInitialDriveForce',
'fDriveInertia',
'fClutchChangeRateScaleUpShift',
'fClutchChangeRateScaleDownShift',
'fInitialDriveMaxFlatVel',
'fLowSpeedTractionLossMult',
},
-- Config for donor engine swap (car from garage is considered to be donor)
donorSwap = {
enabled = true,
swapPrice = 10000, -- Price for donor swapping engine
destroyDonor = true, -- If true, donor vehicle will be destroyed after engine swap
onlyOwned = true, -- If true, only owned vehicles can be used as donor
},
-- Config for engine buy feature
buyEngine = {
enabled = false, -- If true, players can buy engines from NPC
availableEngines = {
{
model = 'nero', -- Model of the engine
label = 'Nero', -- Label that will be displayed in the menu
price = 100000, -- Price of the engine
},
{
model = 'elegy',
label = 'Elegy',
price = 70000,
}
}
},
-- Engine swap NPC cfg
npc = {
coords = vec(472.3840, -1285.2330, 28.5610, 304.0632),
model = 'mp_m_weapwork_01',
spawnDist = 50.0,
showBlip = true,
blip = {
sprite = 1,
color = 1,
label = 'Engine Swap',
}
}
}
-- .d8888b. 888
-- d88P Y88b 888
-- Y88b. 888
-- "Y888b. 888888 8888b. 88888b. .d8888b .d88b.
-- "Y88b. 888 "88b 888 "88b d88P" d8P Y8b
-- "888 888 .d888888 888 888 888 88888888
-- Y88b d88P Y88b. 888 888 888 888 Y88b. Y8b.
-- "Y8888P" "Y888 "Y888888 888 888 "Y8888P "Y8888
-- Enables stance feature. Might be quite performance heavy (adds about 0.04ms for cpu per tick).
Config.EnableStancer = true
-- How many vehicles can be stanced per frame. If you have a lot of vehicles on your server, you might want to lower this number.
Config.MaxStancedVehiclesPerFrame = 3
-- 8888888888 888
-- 888 888
-- 888 888
-- 8888888 888888 .d8888b
-- 888 888 d88P"
-- 888 888 888
-- 888 Y88b. Y88b. d8b
Config.SendClientSideErrorsToServer = false
Config.UI = {
-- Colors used in tablet UI
colors = {
primary = '#f5deb3',
primaryText = '#f5deb3',
},
fullPermsColors = {
primary = '#f5deb3',
primaryText = '#f5deb3',
}
}
-- If set to true, the default handling values will prioritized over handling data read from vehicle.
-- Reading data from vehicle might cause issues if you have any other scripts manipulating handling data.
Config.PrioritizeDefaultHandling = true
SB = {}
SB.FrameworkObject = nil -- Holds ESX or QBCore objects
SB.FrameworkType = ''
SB.ResourceFolderName = nil
-- 88888888b dP
-- 88 88
-- a88aaaa 88d888b. .d8888b. 88d8b.d8b. .d8888b. dP dP dP .d8888b. 88d888b. 88 .dP
-- 88 88' `88 88' `88 88'`88'`88 88ooood8 88 88 88 88' `88 88' `88 88888"
-- 88 88 88. .88 88 88 88 88. ... 88.88b.88' 88. .88 88 88 `8b.
-- dP dP `88888P8 dP dP dP `88888P' 8888P Y8P `88888P' dP dP `YP
local function fetchSharedObject(resourceName, exportName, eventName)
SB.ResourceFolderName = resourceName
try(function()
SB.FrameworkObject = exports[resourceName][exportName]()
TriggerEvent('sb-handlingtuning:frameworkLoaded', SB.FrameworkType)
end, function (err)
print(err)
TriggerEvent(eventName, function (Obj)
SB.FrameworkObject = Obj
end)
Citizen.CreateThread(function()
Citizen.Wait(200)
if not SB.FrameworkObject then
DebugPrint('^8ERROR^7', -1, 'Framework loading failed. Given parameters:')
DebugPrint('^9ERROR^7', -1,
([[
Framework name: ^4%s^7,
event name: ^4%s^7,
export function name: ^4%s^7,
used type: ^4%s^7
]]):format(Config.Framework, eventName, exportName, SB.FrameworkType))
DebugPrint('NOTE', -1, 'If any of the parameters do not match your server settings please check your ^4server_config.lua^7 files')
SB.FrameworkType = 'custom'
end
TriggerEvent('sb-handlingtuning:frameworkLoaded', SB.FrameworkType)
end)
end)
end
local function parseResourceNameFromPath (path)
local index = string.find(path, "/[^/]*$")
return string.sub(path, index + 1)
end
Citizen.CreateThread(function()
-- Assigns framework from config to framework type
if Config.Framework == 'auto' then
-- Check what framework is running
if GetResourceState('es_extended') == 'started' then
SB.FrameworkType = 'esx'
elseif GetResourceState('qb-core') == 'started' then
SB.FrameworkType = 'qbcore'
else
SB.FrameworkType = 'standalone'
end
elseif Config.Framework == 'esx' then
SB.FrameworkType = 'esx'
elseif Config.Framework == 'qbcore' then
SB.FrameworkType = 'qbcore'
elseif Config.Framework == 'standalone' then
SB.FrameworkType = 'standalone'
else
SB.FrameworkType = 'custom'
end
local evName = IsDuplicityVersion() and Config.FrameworkData.SharedObjectEventNameSV or Config.FrameworkData.SharedObjectEventNameCL
if SB.FrameworkType == 'esx' then
fetchSharedObject(parseResourceNameFromPath(GetResourcePath('es_extended')), 'getSharedObject', evName or 'esx:getSharedObject')
elseif SB.FrameworkType == 'qbcore' then
if GetResourceState('qbx_core') == 'started' then
fetchSharedObject('qb-core', 'GetCoreObject', evName or 'QBCore:GetObject')
else
fetchSharedObject(parseResourceNameFromPath(GetResourcePath('qb-core')), 'GetCoreObject', evName or 'QBCore:GetObject')
end
elseif SB.FrameworkType == 'standalone' then
-- Add code for standalone if needed
elseif SB.FrameworkType == 'custom' then
-- Add code for your custom framework
DebugPrint('^8ERROR^7', -1, 'Framework loading failed. No code for custom framework provided, check your ^4*/framework.lua^7 files')
end
-- Detects if database is used
if Config.MySQLScript == 'auto' then
if GetResourceState('oxmysql') then
Config.MySQLScript = 'oxmysql'
elseif GetResourceState('mysql-async') then
Config.MySQLScript = 'MySQL'
else
Config.MySQLScript = 'NO'
end
end
local databaseVersion = mysqlSingleAwait('SELECT VERSION() as version')
DebugPrint('DATABASE', -1, ('Detected database script: ^4%s^7 Database version: ^4%s^7'):format(Config.MySQLScript, databaseVersion?.version))
TriggerEvent('sb-handlingtuning:databaseScriptDetected', Config.MySQLScript)
DebugPrint('FRAMEWORK', -1,
([[
Framework name: ^4%s^7,
event name: ^4%s^7,
export function name: ^4%s^7,
used type: ^4%s^7
]]):format(
Config.Framework, eventName, exportName, SB.FrameworkType))
onServerCB(SVCB.FETCH_FRAMEWORK_RESOURCE_NAME, function (_, cb)
cb(SB.ResourceFolderName)
end)
end)
function Notify(source, msg)
TriggerClientEvent('handlingtuning:Notify', source, msg)
end
RegisterNetEvent('sb-interaction:errorPrint', function(msg)
print(msg)
end)
function ErrorPrint(...)
local text = table.concat({...}, ' ')
local string = ('[^6ERROR^7] - %s ^7'):format(text)
print(string)
end
---Gets what permissions player has
---@param src number
---@return Permissions
function getPlayerPemissions(src)
local identifiers = GetPlayerIdentifiers(src)
for i,v in ipairs(Config.Permissions) do
if v.type == 'identifier' then
for _,identifier in pairs(identifiers) do
if identifier == v.value then return v.permissions end
end
elseif v.type == 'frameworkgroup' then
if SB.FrameworkType == 'qbcore' then
if SB.FrameworkObject and SB.FrameworkObject.Functions.HasPermission(src, v.value) then
return v.permissions
end
elseif SB.FrameworkType == 'esx' then
local xPlayer = SB.FrameworkObject.GetPlayerFromId(src)
if xPlayer.getGroup() == v.value then
return v.permissions
end
else
ErrorPrint('Permission check code for your framework was not set up.')
-- Add code for your framework permission checks
end
elseif v.type == 'ace' then
if IsPlayerAceAllowed(src, v.value) then
return v.permissions
end
end
end
return Config.DefaultPermissions
end
---Gets player identifier used to store presets in database
---@param src number
---@return string | nil identifier
function getPlayerIdentifier(src)
for _, identifier in pairs(GetPlayerIdentifiers(src)) do
if string.find(identifier, Config.PlayerIdentifier) then
return identifier
end
end
ErrorPrint(('Player identifier not found, for player with source: %s.'):format(src))
return nil
end
---Gets player character identifier (UNUSED FOR NOW)
---@param src number
---@return string | nil identifier
function getCharacterIdentifier(src)
if SB.FrameworkType == 'esx' then
local xPlayer = SB.FrameworkObject.GetPlayerFromId(src)
return xPlayer and xPlayer.getIdentifier() or nil
elseif SB.FrameworkType == 'qbcore' then
local ply = SB.FrameworkObject.Functions.GetPlayer(src)
return ply and ply.PlayerData.citizenid or nil
else
return getPlayerIdentifier(src)
end
end
---Checks the framework and registers an inventory item
---@param name string
---@param cb fun(source: number)
function registerItem(name, cb)
if SB.FrameworkType == 'qbcore' then
SB.FrameworkObject.Functions.CreateUseableItem(name, function(source, item)
local Player = SB.FrameworkObject.Functions.GetPlayer(source)
if Player.Functions.GetItemByName(item.name) ~= nil then
cb(source)
end
end)
elseif SB.FrameworkType == 'esx' then
SB.FrameworkObject.RegisterUsableItem(name, function(source)
cb(source)
end)
end
end
---Removes item from player inventory
---@param source number
---@param name string
---@param count number
function removeInventoryItem(source, name, count)
if SB.FrameworkType == 'qbcore' then
local Player = SB.FrameworkObject.Functions.GetPlayer(source)
Player.Functions.RemoveItem(name, count)
elseif SB.FrameworkType == 'esx' then
local xPlayer = SB.FrameworkObject.GetPlayerFromId(source)
xPlayer.removeInventoryItem(name, count)
end
end
---Check if player has enough money
---@param source number
---@param requiredAmount number
function checkAccountMoney(source, requiredAmount)
if SB.FrameworkType == 'qbcore' then
local Player = SB.FrameworkObject.Functions.GetPlayer(source)
if type(Player.Functions.GetMoney('cash')) == 'number' then
return Player.Functions.GetMoney('cash') >= requiredAmount
elseif type(Player.Functions.GetMoney('cash')) == 'table' then
return Player.Functions.GetMoney('cash').money >= requiredAmount
end
elseif SB.FrameworkType == 'esx' then
local xPlayer = SB.FrameworkObject.GetPlayerFromId(source)
return xPlayer.getAccount('money').money >= requiredAmount
end
end
---Remove account money
---@param source number
---@param amount number
function removeAccountMoney(source, amount)
if SB.FrameworkType == 'qbcore' then
local Player = SB.FrameworkObject.Functions.GetPlayer(source)
Player.Functions.RemoveMoney('cash', amount)
elseif SB.FrameworkType == 'esx' then
local xPlayer = SB.FrameworkObject.GetPlayerFromId(source)
xPlayer.removeAccountMoney('money', amount)
end
end
---Checks if vehicle can have engine swapper
---@param plate string
---@param modelHash number
---@return boolean
function isOwnedVehicleSuitableForEngineSwap(plate, modelHash)
return true
end
---Checks if vehicle can be used as engine donor
---@param plate ?string
---@param modelHash number
---@return boolean
function isOwnedVehicleSuitableForEngineDonor(plate, modelHash)
--Checks if vehicle data is saved in database
local res = mysqlSingleAwait('SELECT * FROM sb_vehicles WHERE model_hash = @modelHash', {modelHash = modelHash})
if not res then return false end
return true
end
function onDonorUsed(donorPlate)
if Config.EngineSwap.donorSwap.destroyDonor then
mysqlQuery(('DELETE FROM %s WHERE REPLACE(%s, \' \', \'\') LIKE \'%s\''):format(
DB_INFO.tableName,
DB_INFO.plateColumn,
'%'..string.removeSpaces(donorPlate)..'%'
), {}, function(res)
if res then
DebugPrint('DATABASE', 2, 'Deleted donor vehicle with plate: ^4%s^7', donorPlate)
end
end)
end
end
onServerCB('sb-handlingtuning:getPlayerPermissions', function(source, cb)
cb(getPlayerPemissions(source))
end)
function CheckFakePlate(plate)
local fakePlateResource = GetConvar('handlingtuning:fakePlateCheckExportResource', 'false')
local fakePlateFunction = GetConvar('handlingtuning:fakePlateCheckExportEvent', 'false')
if fakePlateResource == 'false' or fakePlateFunction == 'false' then
return plate
end
try(function()
local fakePlate = exports[fakePlateResource][fakePlateFunction](nil, plate)
if fakePlate then
plate = fakePlate
end
end, function(err)
print(err)
end)
return plate
end
-- 8888ba.88ba .d88888b .88888. dP
-- 88 `8b `8b 88. "' d8' `8b 88
-- 88 88 88 dP dP `Y88888b. 88 88 88
-- 88 88 88 88 88 `8b 88 db 88 88
-- 88 88 88 88. .88 d8' .8P Y8. Y88P 88
-- dP dP dP `8888P88 Y88888P `8888PY8b 88888888P
-- .88
-- d8888P
---Checks the database table to see if vehicle is owned
---@param plate string
---@return boolean
function isVehicleOwned(plate)
plate = CheckFakePlate(plate)
if plate and Config.UseDatabaseHandlingSaving then
local res = mysqlSingleAwait(
('SELECT plate FROM %s WHERE REPLACE(%s, \' \', \'\') LIKE \'%s\''):format(
DB_INFO.tableName,
DB_INFO.plateColumn,
'%'..string.removeSpaces(plate)..'%'
), {})
-- Checks if the plate actually matches the one store in database
-- as the mysql query removes all spaces between characters
return res and string.trim(res.plate) == string.trim(plate)
end
return false
end
---Get owned vehicle model hash
---@param plate string
---@return number | nil
function getOwnedVehicleModel(plate)
plate = CheckFakePlate(plate)
if plate and Config.UseDatabaseHandlingSaving then
local res = mysqlSingleAwait(
('SELECT * FROM %s WHERE REPLACE(%s, \' \', \'\') LIKE \'%s\''):format(
DB_INFO.tableName,
DB_INFO.plateColumn,
'%'..string.removeSpaces(plate)..'%'
), {})
-- Checks if the plate actually matches the one store in database
-- as the mysql query removes all spaces between characters
if res and string.trim(res.plate) == string.trim(plate) then
if SB.FrameworkType == 'qbcore' then
return res.hash
elseif SB.FrameworkType == 'esx' then
local vehicleData = json.decode(res.vehicle)
return vehicleData.model
end
end
end
return nil
end
function getOwnedVehicles(source)
if Config.UseDatabaseHandlingSaving then
local res = mysqlQueryAwait(
('SELECT * FROM %s WHERE %s = ?'):format(
DB_INFO.tableName,
DB_INFO.ownerColumn
), {getCharacterIdentifier(source)})
local ownedVehicles = {}
if res then
if SB.FrameworkType == 'qbcore' then
for i,v in ipairs(res) do
table.insert(ownedVehicles, {
plate = v.plate,
modelHash = v.hash,
})
end
elseif SB.FrameworkType == 'esx' then
for i,v in ipairs(res) do
local vehicleData = json.decode(v.vehicle)
table.insert(ownedVehicles, {
plate = v.plate,
modelHash = vehicleData.model,
})
end
end
end
return ownedVehicles
end
return {}
end
---Saves handling for vehicle in database
---@param plate string
---@param handlingData table
---@param model number
function saveNewHandling(plate, handlingData, model)
if Config.UseDatabaseHandlingSaving then
mysqlQuery('INSERT INTO sb_handlings (plate, differences, drift, stance, model_hash) VALUES (@plate, @differences, @drift, @stance, @model_hash) ON DUPLICATE KEY UPDATE differences = @differences, model_hash = @model_hash, drift = @drift, stance = @stance', {
['plate'] = CheckFakePlate(plate),
['differences'] = json.encode(handlingData.valuesDifferences),
['drift'] = json.encode(handlingData.drift),
['stance'] = json.encode(handlingData.stance),
['model_hash'] = model
})
end
end
---Async Query
---@param query string
---@param params MySQLParameters
---@param cb fun(result: QueryResult)
function mysqlQuery(query, params, cb)
if Config.MySQLScript == 'oxmysql' then
MySQL.query(query, params, cb)
elseif Config.MySQLScript == 'MySQL' then
MySQL.Async.fetchAll(query, params, cb)
end
end
---Sync query
---@param query string
---@param params MySQLParameters
---@return QueryResult
function mysqlQueryAwait(query, params)
if Config.MySQLScript == 'oxmysql' then
return MySQL.query.await(query, params)
elseif Config.MySQLScript == 'MySQL' then
return MySQL.Sync.fetchAll(query, params)
end
end
---Async Single result Query
---@param query string
---@param params MySQLParameters
---@param cb fun(result: MySQLRow)
function mysqlSingle(query, params, cb)
if Config.MySQLScript == 'oxmysql' then
MySQL.single(query, params, cb)
elseif Config.MySQLScript == 'MySQL' then
MySQL.Async.fetchAll(query, params, function(res) cb(res[1]) end)
end
end
---Sync Single result query
---@param query string
---@param params MySQLParameters
---@return MySQLRow
function mysqlSingleAwait(query, params)
if Config.MySQLScript == 'oxmysql' then
return MySQL.single.await(query, params)
elseif Config.MySQLScript == 'MySQL' then
return MySQL.Sync.fetchAll(query, params)[1]
end
end
---Saves handling preset
---@param name string
---@param handlingData table
---@param carName string
RegisterNetEvent('sb-handlingtuning:sv:saveHandling', function(name, handlingData, carName)
local src = source
if Config.UseDatabasePresetsSaving then
mysqlQuery('INSERT INTO sb_handling_presets (identifier, handling_data, car_name, name) VALUES (@id, @handlingData, @carName, @handlingName)', {
['id'] = getPlayerIdentifier(src),
['handlingData'] = json.encode(handlingData),
['carName'] = carName,
['handlingName'] = name
})
end
end)
---Deletes handling preset
---@param presetId number
RegisterNetEvent('sb-handlingtuning:sv:deleteHandling', function(presetId)
local src = source
mysqlQuery('DELETE FROM sb_handling_presets WHERE `id` = @id AND `identifier` = @identifier', {id = presetId, identifier = getPlayerIdentifier(src)})
end)
---Shares handlings preset with another player
---@param targetId number
---@param preset table
RegisterNetEvent('sb-handlingtuning:sv:sharePreset', function(targetId, preset)
if Config.UseDatabasePresetsSaving then
local src = source
if getPlayerIdentifier(targetId) then
mysqlQuery('INSERT INTO sb_handling_presets (identifier, handling_data, car_name, name, creator) VALUES (@id, @handlingData, @carName, @handlingName, @plyName)', {
['id'] = getPlayerIdentifier(tonumber(targetId)),
['handlingData'] = json.encode(preset.handlingData),
['carName'] = preset.carName,
['handlingName'] = preset.handlingName,
['plyName'] = GetPlayerName(src),
})
Notify(tonumber(targetId), "Someone shared a new tuning tablet handling preset with you.")
else
Notify(src, "Player is not online.")
end
end
end)
---Gets handling presets from database
lib.callback.register('sb-handlingtuning:sv:getHandlingPresets', function(source)
local src = source
if Config.UseDatabasePresetsSaving then
local result = mysqlQueryAwait('SELECT * FROM sb_handling_presets WHERE identifier = @id', {id = getPlayerIdentifier(src)})
if result then
for i = 1, #result do
result[i].carName = result[i].car_name
result[i].handlingName = result[i].name
result[i].handlingData = json.decode(result[i].handling_data)
end
return result
else
return nil
end
else
return {}
end
end)
-- .d8888b. 888
-- d88P Y88b 888
-- 888 888 888
-- 888 .d88b. 88888b.d88b. 88888b.d88b. 8888b. 88888b. .d88888 .d8888b
-- 888 d88""88b 888 "888 "88b 888 "888 "88b "88b 888 "88b d88" 888 88K
-- 888 888 888 888 888 888 888 888 888 888 .d888888 888 888 888 888 "Y8888b.
-- Y88b d88P Y88..88P 888 888 888 888 888 888 888 888 888 888 Y88b 888 X88
-- "Y8888P" "Y88P" 888 888 888 888 888 888 "Y888888 888 888 "Y88888 88888P'
RegisterCommand(Config.TabletCommand, function(s, a)
if getPlayerPemissions(s)?.tabletCommand then
TriggerClientEvent('sb-handlingtuning:cl:openTablet', s, false, getPlayerPemissions(s))
end
end)
RegisterCommand(Config.SmallTabletCommand, function(s, a)
if getPlayerPemissions(s)?.tabletCommand then
TriggerClientEvent('sb-handlingtuning:cl:openTablet', s, true, getPlayerPemissions(s))
end
end)
RegisterCommand('admin:cars', function(s, a)
if getPlayerPemissions(s)?.writeDefault then
emitNet('sb-handlingtuning:cl:openAdminMenu', s)
end
end)
-- 8888888 888
-- 888 888
-- 888 888
-- 888 888888 .d88b. 88888b.d88b. .d8888b
-- 888 888 d8P Y8b 888 "888 "88b 88K
-- 888 888 88888888 888 888 888 "Y8888b.
-- 888 Y88b. Y8b. 888 888 888 X88
-- 8888888 "Y888 "Y8888 888 888 888 88888P'
Citizen.CreateThread(function()
local function applyModCallback(source, res, modData)
if res.success then
if modData?.itemData?.consume then
removeInventoryItem(source, modData.itemData.name or modData.uniqueId, 1)
end
else
Notify(source, locale(res.message))
end
end
-- Registering tablet items
registerItem(Config.TabletItemName, function(source)
TriggerClientEvent("sb-handlingtuning:cl:openTablet", source, false, Config.DefaultPermissions)
end)
registerItem(Config.SmallTabletItemName, function(source)
TriggerClientEvent("sb-handlingtuning:cl:openTablet", source, true, Config.DefaultPermissions)
end)
-- Registering custom mods items
for i,v in ipairs(Config.CustomMods) do
v.handlingData.uniqueId = v.uniqueId
if v.itemData then
registerItem(v.itemData.name or v.uniqueId, function(source)
lib.callback('sb-handlingtuning:cl:applyMod', source, function(res)
applyModCallback(source, res, v)
end, v)
end)
end
end
onServerCB('sb-handlingtuning:sv:useModItem', function(source, cb, slot)
local slot = exports.ox_inventory:GetSlot(source, slot)
local slotItem = slot and slot.name
if not slotItem then
cb(false)
return
end
installCustomMod(source, slotItem)
end)
function installCustomMod(source, itemName)
local itemData
for i,v in ipairs(Config.CustomMods) do
if v.itemData?.name == itemName or (v.itemData?.name == nil and v.uniqueId == itemName) then
itemData = v
break
end
end
if not itemData then
return
end
lib.callback('sb-handlingtuning:cl:applyMod', source, function(res)
applyModCallback(source, res, itemData)
end, itemData)
end
end)
-- 8888888888 888
-- 888 888
-- 888 888
-- 8888888 888888 .d8888b
-- 888 888 d88P"
-- 888 888 888
-- 888 Y88b. Y88b. d8b
-- 8888888888 "Y888 "Y8888P Y8P
{
"fDownforceModifier": {
"label": "fDownforceModifierLabel",
"description": "fDownforceModifierDescription",
"min": {
"changeLimit": "0.5",
"value": "1.15"
},
"max": {
"changeLimit": "0.3",
"value": "300"
},
"group": "groupAero"
},
"fDriveInertia": {
"label": "fDriveInertiaLabel",
"description": "fDriveInertiaDescription",
"min": {
"changeLimit": "0.3",
"value": "0.1"
},
"max": {
"changeLimit": "0.2",
"value": "2.5"
},
"group": "groupEngine"
},
"fPetrolTankVolume": {
"label": "fPetrolTankVolumeLabel",
"description": "fPetrolTankVolumeDescription",
"max": {
"changeLimit": "20.5",
"value": "5000"
},
"min": {
"changeLimit": "20.5",
"value": "0"
}
},
"fRollCentreHeightRear": {
"label": "fRollCentreHeightRearLabel",
"description": "fRollCentreHeightRearDescription",
"min": {
"changeLimit": "0.5",
"value": "0"
},
"max": {
"changeLimit": "0.5",
"value": "1.2"
},
"group": "groupSuspension"
},
"fDriveBiasFront": {
"label": "fDriveBiasFrontLabel",
"description": "fDriveBiasFrontDescription",
"min": {
"changeLimit": "20",
"value": "0"
},
"max": {
"changeLimit": "20",
"value": "1"
},
"group": "groupEngine"
},
"fPercentSubmerged": {
"label": "fPercentSubmergedLabel",
"description": "fPercentSubmergedDescription",
"min": {
"changeLimit": "20",
"value": "45"
},
"max": {
"changeLimit": "20",
"value": "200"
},
"group": "groupOther"
},
"fEngineDamageMult": {
"label": "fEngineDamageMultLabel",
"description": "fEngineDamageMultDescription",
"min": {
"changeLimit": "0.5",
"value": "0.01"
},
"max": {
"changeLimit": "0.5",
"value": "2.5"
},
"group": "groupDamage"
},
"fHandBrakeForce": {
"label": "fHandBrakeForceLabel",
"description": "fHandBrakeForceDescription",
"min": {
"changeLimit": "2.0",
"value": "0.01"
},
"max": {
"changeLimit": "1.5",
"value": "6"
},
"group": "groupBrakes"
},
"fLowSpeedTractionLossMult": {
"label": "fLowSpeedTractionLossMultLabel",
"description": "fLowSpeedTractionLossMultDescription",
"min": {
"changeLimit": "0.1",
"value": "0"
},
"max": {
"changeLimit": "0.5",
"value": "2.2"
},
"group": "groupSuspension"
},
"fWeaponDamageScaledToVehHealthMult": {
"label": "fWeaponDamageScaledToVehHealthMultLabel",
"description": "fWeaponDamageScaledToVehHealthMultDescription",
"min": {
"changeLimit": "0.2",
"value": "0.0"
},
"max": {
"changeLimit": "0.2",
"value": "0.5"
},
"group": "groupDamage"
},
"fOilVolume": {
"label": "fOilVolumeLabel",
"description": "fOilVolumeDescription",
"max": {
"changeLimit": "2.0",
"value": "10"
},
"min": {
"changeLimit": "2.0",
"value": "0"
}
},
"fSeatOffsetDistX": {
"label": "fSeatOffsetDistXLabel",
"description": "fSeatOffsetDistXDescription",
"max": {
"changeLimit": "0.0",
"value": "0.3"
},
"min": {
"changeLimit": "0.0",
"value": "-0.2"
}
},
"fTractionCurveLateral": {
"label": "fTractionCurveLateralLabel",
"description": "fTractionCurveLateralDescription",
"min": {
"changeLimit": "0.5",
"value": "1"
},
"max": {
"changeLimit": "0.25",
"value": "120"
},
"group": "groupTraction"
},
"fInitialDragCoeff": {
"label": "fInitialDragCoeffLabel",
"description": "fInitialDragCoeffDescription",
"min": {
"changeLimit": "0.6",
"value": "0.9"
},
"max": {
"changeLimit": "2.5",
"value": "300"
},
"group": "groupAero"
},
"fSuspensionBiasFront": {
"label": "fSuspensionBiasFrontLabel",
"description": "fSuspensionBiasFrontDescription",
"min": {
"changeLimit": "0.3",
"value": "0"
},
"max": {
"changeLimit": "0.3",
"value": "0.85"
},
"group": "groupSuspension"
},
"fTractionCurveMax": {
"label": "fTractionCurveMaxLabel",
"description": "fTractionCurveMaxDescription",
"min": {
"changeLimit": "1.3",
"value": "0"
},
"max": {
"changeLimit": "0.1",
"value": "3.7"
},
"group": "groupTraction"
},
"fSuspensionLowerLimit": {
"label": "fSuspensionLowerLimitLabel",
"description": "fSuspensionLowerLimitDescription",
"min": {
"changeLimit": "0.3",
"value": "-0.36"
},
"max": {
"changeLimit": "0.3",
"value": "0.1"
},
"group": "groupSuspension"
},
"nMonetaryValue": {
"label": "nMonetaryValueLabel",
"description": "nMonetaryValueDescription",
"max": {
"changeLimit": "0.0",
"value": "500000"
},
"min": {
"changeLimit": "0.0",
"value": "10000"
}
},
"fSuspensionForce": {
"label": "fSuspensionForceLabel",
"description": "fSuspensionForceDescription",
"min": {
"changeLimit": "0.3",
"value": "0"
},
"max": {
"changeLimit": "0.2",
"value": "9"
},
"group": "groupSuspension"
},
"fMass": {
"label": "fMassLabel",
"description": "fMassDescription",
"min": {
"changeLimit": "200",
"value": "100"
},
"max": {
"changeLimit": "500",
"value": "20000"
},
"group": "groupBody"
},
"fSeatOffsetDistY": {
"label": "fSeatOffsetDistYLabel",
"description": "fSeatOffsetDistYDescription",
"max": {
"changeLimit": "0.0",
"value": "0.3"
},
"min": {
"changeLimit": "0.0",
"value": "-0.6"
}
},
"fSeatOffsetDistZ": {
"label": "fSeatOffsetDistZLabel",
"description": "fSeatOffsetDistZDescription",
"max": {
"changeLimit": "0.0",
"value": "0.5"
},
"min": {
"changeLimit": "0.0",
"value": "-0.4"
}
},
"fSuspensionRaise": {
"label": "fSuspensionRaiseLabel",
"description": "fSuspensionRaiseDescription",
"min": {
"changeLimit": "0.2",
"value": "-0.085"
},
"max": {
"changeLimit": "0.2",
"value": "0.35"
},
"group": "groupSuspension"
},
"fTractionSpringDeltaMax": {
"label": "fTractionSpringDeltaMaxLabel",
"description": "fTractionSpringDeltaMaxDescription",
"min": {
"changeLimit": "0.05",
"value": "0.02"
},
"max": {
"changeLimit": "0.05",
"value": "0.5"
},
"group": "groupTraction"
},
"fBrakeForce": {
"label": "fBrakeForceLabel",
"description": "fBrakeForceDescription",
"min": {
"changeLimit": "0.5",
"value": "0.001"
},
"max": {
"changeLimit": "0.35",
"value": "5"
},
"group": "groupBrakes"
},
"fAntiRollBarForce": {
"label": "fAntiRollBarForceLabel",
"description": "fAntiRollBarForceDescription",
"min": {
"changeLimit": "0.2",
"value": "0"
},
"max": {
"changeLimit": "0.2",
"value": "3"
},
"group": "groupSuspension"
},
"fAntiRollBarBiasFront": {
"label": "fAntiRollBarBiasFrontLabel",
"description": "fAntiRollBarBiasFrontDescription",
"min": {
"changeLimit": "0.2",
"value": "0"
},
"max": {
"changeLimit": "0.2",
"value": "1"
},
"group": "groupSuspension"
},
"fBrakeBiasFront": {
"label": "fBrakeBiasFrontLabel",
"description": "fBrakeBiasFrontDescription",
"min": {
"changeLimit": "0.5",
"value": "0"
},
"max": {
"changeLimit": "0.5",
"value": "0.8"
},
"group": "groupBrakes"
},
"fTractionCurveMin": {
"label": "fTractionCurveMinLabel",
"description": "fTractionCurveMinDescription",
"min": {
"changeLimit": "0.3",
"value": "0"
},
"max": {
"changeLimit": "0.25",
"value": "3.5"
},
"group": "groupTraction"
},
"nInitialDriveGears": {
"label": "nInitialDriveGearsLabel",
"description": "nInitialDriveGearsDescription",
"min": {
"changeLimit": "2",
"value": "1"
},
"max": {
"changeLimit": "2",
"value": "10"
},
"group": "groupEngine"
},
"fBoostMaxSpeed": {
"label": "fBoostMaxSpeedLabel",
"description": "fBoostMaxSpeedDescription",
"min": {
"changeLimit": "1.5",
"value": "15"
},
"max": {
"changeLimit": "1.5",
"value": "87.5"
},
"group": "groupEngine"
},
"fSuspensionUpperLimit": {
"label": "fSuspensionUpperLimitLabel",
"description": "fSuspensionUpperLimitDescription",
"min": {
"changeLimit": "0.3",
"value": "0"
},
"max": {
"changeLimit": "0.3",
"value": "0.8"
},
"group": "groupSuspension"
},
"fClutchChangeRateScaleDownShift": {
"label": "fClutchChangeRateScaleDownShiftLabel",
"description": "fClutchChangeRateScaleDownShiftDescription",
"min": {
"changeLimit": "2.0",
"value": "0.3"
},
"max": {
"changeLimit": "1.0",
"value": "9"
},
"group": "groupEngine"
},
"fTractionBiasFront": {
"label": "fTractionBiasFrontLabel",
"description": "fTractionBiasFrontDescription",
"min": {
"changeLimit": "0.15",
"value": "0.325"
},
"max": {
"changeLimit": "0.15",
"value": "0.95"
},
"group": "groupTraction"
},
"fInitialDriveForce": {
"label": "fInitialDriveForceLabel",
"description": "fInitialDriveForceDescription",
"min": {
"changeLimit": "0.1",
"value": "0"
},
"max": {
"changeLimit": "0.01",
"value": "50"
},
"group": "groupEngine"
},
"fClutchChangeRateScaleUpShift": {
"label": "fClutchChangeRateScaleUpShiftLabel",
"description": "fClutchChangeRateScaleUpShiftDescription",
"min": {
"changeLimit": "2.0",
"value": "0.3"
},
"max": {
"changeLimit": "0.5",
"value": "9"
},
"group": "groupEngine"
},
"fInitialDriveMaxFlatVel": {
"label": "fInitialDriveMaxFlatVelLabel",
"description": "fInitialDriveMaxFlatVelDescription",
"min": {
"changeLimit": "30.0",
"value": "10"
},
"max": {
"changeLimit": "4.0",
"value": "328.6"
},
"group": "groupEngine"
},
"fPopUpLightRotation": {
"label": "fPopUpLightRotationLabel",
"description": "fPopUpLightRotationDescription",
"max": {
"changeLimit": "0.2",
"value": "45"
},
"min": {
"changeLimit": "0.2",
"value": "17.5"
}
},
"fCollisionDamageMult": {
"label": "fCollisionDamageMultLabel",
"description": "fCollisionDamageMultDescription",
"min": {
"changeLimit": "0.5",
"value": "0.005"
},
"max": {
"changeLimit": "0.5",
"value": "2"
},
"group": "groupDamage"
},
"fRollCentreHeightFront": {
"label": "fRollCentreHeightFrontLabel",
"description": "fRollCentreHeightFrontDescription",
"min": {
"changeLimit": "0.1",
"value": "0"
},
"max": {
"changeLimit": "0.1",
"value": "1.2"
},
"group": "groupSuspension"
},
"fWeaponDamageMult": {
"label": "fWeaponDamageMultLabel",
"description": "fWeaponDamageMultDescription",
"min": {
"changeLimit": "0.1",
"value": "0.03"
},
"max": {
"changeLimit": "0.1",
"value": "4"
},
"group": "groupDamage"
},
"vecInertiaMultiplier": {
"label": "vecInertiaMultiplierLabel",
"description": "vecInertiaMultiplierDescription",
"max": {
"changeLimit": "0.5",
"value": "10"
},
"min": {
"changeLimit": "0.5",
"value": "-5"
}
},
"fCamberStiffnesss": {
"label": "fCamberStiffnesssLabel",
"description": "fCamberStiffnesssDescription",
"min": {
"changeLimit": "0.1",
"value": "0"
},
"max": {
"changeLimit": "0.1",
"value": "1.12"
},
"group": "groupSuspension"
},
"fSuspensionReboundDamp": {
"label": "fSuspensionReboundDampLabel",
"description": "fSuspensionReboundDampDescription",
"min": {
"changeLimit": "0.5",
"value": "0"
},
"max": {
"changeLimit": "0.5",
"value": "10.8"
},
"group": "groupSuspension"
},
"vecCentreOfMassOffset": {
"label": "vecCentreOfMassOffsetLabel",
"description": "vecCentreOfMassOffsetDescription",
"max": {
"changeLimit": "0.5",
"value": "10"
},
"min": {
"changeLimit": "0.5",
"value": "-5"
}
},
"fSuspensionCompDamp": {
"label": "fSuspensionCompDampLabel",
"description": "fSuspensionCompDampDescription",
"min": {
"changeLimit": "2.0",
"value": "0"
},
"max": {
"changeLimit": "2.0",
"value": "8"
},
"group": "groupSuspension"
},
"fDeformationDamageMult": {
"label": "fDeformationDamageMultLabel",
"description": "fDeformationDamageMultDescription",
"min": {
"changeLimit": "0.1",
"value": "0"
},
"max": {
"changeLimit": "0.9",
"value": "5"
},
"group": "groupDamage"
},
"fTractionLossMult": {
"label": "fTractionLossMultLabel",
"description": "fTractionLossMultDescription",
"min": {
"changeLimit": "0.1",
"value": "0"
},
"max": {
"changeLimit": "0.3",
"value": "1.4"
},
"group": "groupTraction"
},
"fRocketBoostCapacity": {
"label": "fRocketBoostCapacityLabel",
"description": "fRocketBoostCapacityDescription",
"min": {
"changeLimit": "1.5",
"value": "3"
},
"max": {
"changeLimit": "1.5",
"value": "20"
},
"group": "groupEngine"
},
"fSteeringLock": {
"label": "fSteeringLockLabel",
"description": "fSteeringLockDescription",
"min": {
"changeLimit": "7.0",
"value": "20"
},
"max": {
"changeLimit": "7.0",
"value": "90"
},
"group": "groupSuspension"
}
}