Squirrel Bracket Documentation
TebexDiscord
Handling Tablet V2
Handling Tablet V2
  • 📃Introduction
  • 💽Installation
  • ✏️Configuration
  • ⚙️API
  • ❓Common Issues / FAQ
  • 📰Changelog
Powered by GitBook
On this page
  • Custom mods
  • Permissions
  • Configuration files

Was this helpful?

Configuration

Detailed configuration information

PreviousInstallationNextAPI

Last updated 1 month ago

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.

Custom mods

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.

Fields

  • 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.

Example

Config.CustomMods = {
    -- ...other mods
    {
        uniqueId = 'car_mod_engine_ecu',
        itemData = ,
        label = 'ECU',
        identifier = ,
        save = true,
        handlingData = {
            enableHandlingFields =
            {
                
            }, 
            ,
            
        },
    },
    -- ...other mods
}

Permissions

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.

Config.Permissions

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.

Configuration files

✏️
Additional Handlings API
https://github.com/daZepelin/sb-handlingtuning/blob/master/configs/client_config.lua
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
https://github.com/daZepelin/sb-handlingtuning/blob/master/configs/config.lua
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
https://github.com/daZepelin/sb-handlingtuning/blob/master/configs/server_config.lua
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
https://github.com/daZepelin/sb-handlingtuning/blob/master/configs/handling_values.json
{
  "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"
  }
}