How to make hybrids for beamng export [Guide]

A cheeky doc outlining how to make hybrids when exporting from Automation to BeamNG.Drive

This guide will let you make P2 hybrid systems where we have our motor and electric motor both powering a DCT gearbox. You can switch between drive modes (like ev only mode, hybrid mode, ice only mode) seamlessly and the motor powers the gearbox so if FWD/RWD/AWD, it will just work and only power the wheels you want. It also uses a battery inside your car so it actually drains this battery and refills it as required. The max hp, max rpm, torque, and battery capacity are all editable inside the BeamNG vehicle customisation sliders.

The google doc

document/d/1DFwfdOBJU3TVnbBBjwbuPeXt7W2JI9fSNKjsPfnYmow

This is only the start and I will make more hybrid systems for different transmission types. I will maybe also publish a script/app that can do this stuff automatically for people. Stay tuned!

1 Like

This guide will let you make p2 hybrid systems where we have our motor and electric motor both powering a DCT gearbox. You can switch between drive modes (like ev only mode, hybrid mode, ice only mode) seamlessly and the motor powers the gearbox so if FWD/RWD/AWD, it will just work and only power the wheels you want. It also uses a battery inside your car so it actually drains this battery and refills it as required. The max hp, max rpm, torque, and battery capacity are all editable inside the BeamNG vehicle customisation sliders.

To add this to your car, make a copy of this document then make sure to:

Replace all instances of with the name of the mod you exported your car under.

Replace all instances of with the name of the car you exported your trim under.

Replace all instances of “abcde” with whatever the 5 digit code of the trim you want to make work is. It will be something like “d6931” or “769e5”.

Step 1:

Put the code from shiftLogic-p2DCTGearbox.lua into /unpacked//lua/vehicle/controller/shiftLogic-p2DCTGearbox.lua

Step 2:

Put the code from p2DCTGearbox.lua into /unpacked//vehicles//lua/powertrain/p2DCTGearbox.lua

Step 3:

Put the code from camso_advawd.lua into \unpacked\\vehicles\\lua\controller\drivingDynamics\actuators\camso_advawd.lua

Step 4:

Replace \unpacked\\vehicles\\abcde\camso_electric_battery_abcde.jbeam with the code in camso_electric_battery_abcde.jbeam

Step 5:

Replace \unpacked\\vehicles\\abcde\camso_transmission_abcde.jbeam with the code in camso_transmission_abcde.jbeam

Step 6:

Replace \unpacked\\vehicles\\abcde\camso_dse_drivemodes_abcde.jbeam with the code in camso_dse_drivemodes_abcde.jbeam

Step 7:

In \unpacked\\vehicles\\abcde\abcde_main.jbeam, we want to edit the slots. It should look something like this:

Below the “Camso_engine”, add:

[“Camso_Hybrid_Battery”,“Camso_Battery_abcde”,“Hybrid Battery”],

Scroll down a little and you will then see the controller block looking like this:

Replace that with this:

    "controller": \[

        \["fileName"\],

        \["vehicleController", {"shiftLogicName": "p2DCTGearbox"}\],

        \["cefaero"\]

    \],



    "vehicleController": {

        "shiftLogicName": "dctGearbox"

    },

shiftLogic-p2DCTGearbox.lua

-- This Source Code Form is subject to the terms of the bCDDL, v. 1.1.
-- If a copy of the bCDDL was not distributed with this
-- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt


local M = {}


local max = math.max
local min = math.min
local abs = math.abs


local constants = {rpmToAV = 0.104719755, avToRPM = 9.549296596425384}


local newDesiredGearIndex = 0
local previousGearIndex = 0
local shiftAggression = 1
local gearbox = nil
local engine = nil


local sharedFunctions = nil
local gearboxAvailableLogic = nil
local gearboxLogic = nil


M.gearboxHandling = nil
M.timer = nil
M.timerConstants = nil
M.inputValues = nil
M.shiftPreventionData = nil
M.shiftBehavior = nil
M.smoothedValues = nil


M.currentGearIndex = 0
M.maxGearIndex = 0
M.minGearIndex = 0
M.throttle = 0
M.brake = 0
M.clutchRatio = 0
M.shiftingAggression = 0
M.isArcadeSwitched = false
M.isSportModeActive = false
M.isManualModeActive = false


M.smoothedAvgAVInput = 0
M.rpm = 0
M.idleRPM = 0
M.maxRPM = 0


M.engineThrottle = 0
M.engineLoad = 0
M.engineTorque = 0
M.flywheelTorque = 0
M.gearboxTorque = 0


M.ignition = true
M.isEngineRunning = 0


M.oilTemp = 0
M.waterTemp = 0
M.checkEngine = false


M.energyStorages = {}


local automaticHandling = {
    availableModes = {"P", "R", "N", "D", "S", "1", "2", "M"},
    hShifterModeLookup = {[-1] = "R", [0] = "N", "P", "D", "S", "2", "1", "M1"},
    availableModeLookup = {},
    existingModeLookup = {},
    modeIndexLookup = {},
    modes = {},
    mode = nil,
    modeIndex = 0,
    maxAllowedGearIndex = 0,
    minAllowedGearIndex = 0,
    autoDownShiftInM = true,
    defaultForwardMode = "D"
}


local clutchHandling = {
    clutchLaunchTargetAV = 0,
    clutchLaunchStartAV = 0,
    clutchLaunchIFactor = 0,
    revMatchThrottle = 0.5,
    didRevMatch = false,
    didCutIgnition = false,
    isClutchOpenLaunch = false
}


local dct = {
    access1 = {clutchRatioName = "clutchRatio1", gearIndexName = "gearIndex1", setGearIndexName = "setGearIndex1"},
    access2 = {clutchRatioName = "clutchRatio2", gearIndexName = "gearIndex2", setGearIndexName = "setGearIndex2"},
    primaryAccess = nil,
    secondaryAccess = nil,
    clutchTime = 0
}


local ctrlDebugTimer = 0
local lastLoggedMode = nil
local lastLoggedGear = nil
local lastLoggedHybridMode = nil
local lastShiftState = "inGear"
local isEngineAllowed = false
local wasEngineAllowed = false


local function areShiftLEDsInUse()
    return M.gearboxHandling.behavior ~= "arcade" and M.isManualModeActive
end


local function getGearName()
    local modePrefix = ""
    if automaticHandling.mode == "S" and M.currentGearIndex > 0 then
        modePrefix = "S"
    elseif string.sub(automaticHandling.mode, 1, 1) == "M" then
        modePrefix = "M"
    end
    if M.currentGearIndex ~= 0 then
        return modePrefix ~= "" and modePrefix .. tostring(M.currentGearIndex) or automaticHandling.mode
    else
        return automaticHandling.mode
    end
end


local function getGearPosition()
    return (automaticHandling.modeIndex - 1) / (#automaticHandling.modes - 1), automaticHandling.modeIndex
end


local function applyGearboxModeRestrictions()
    local manualModeIndex
    if string.sub(automaticHandling.mode, 1, 1) == "M" then
        manualModeIndex = string.sub(automaticHandling.mode, 2)
    end
    local maxGearIndex = gearbox.maxGearIndex
    local minGearIndex = gearbox.minGearIndex
    if automaticHandling.mode == "1" then
        maxGearIndex = 1
        minGearIndex = 1
    elseif automaticHandling.mode == "2" then
        maxGearIndex = 2
        minGearIndex = 1
    elseif manualModeIndex then
        maxGearIndex = manualModeIndex
        minGearIndex = manualModeIndex
    end


    automaticHandling.maxGearIndex = maxGearIndex
    automaticHandling.minGearIndex = minGearIndex
end


local function gearboxBehaviorChanged(behavior)
    gearboxLogic = gearboxAvailableLogic[behavior]
    M.updateGearboxGFX = gearboxLogic.inGear
    M.shiftUp = gearboxLogic.shiftUp
    M.shiftDown = gearboxLogic.shiftDown
    M.shiftToGearIndex = gearboxLogic.shiftToGearIndex
end


local function calculateShiftAggression()
    local gearRatioDifference = abs(gearbox.gearRatios[previousGearIndex] - gearbox.gearRatios[newDesiredGearIndex])
    local inertiaCoef = linearScale(engine.inertia, 0.1, 0.5, 0.1, 1)
    local gearRatioCoef = linearScale(gearRatioDifference * inertiaCoef, 0.5, 1, 1, 0.5)
    local aggressionCoef = linearScale(M.smoothedValues.drivingAggression, 0.5, 1, 0.1, 1)


    shiftAggression = clamp(gearRatioCoef * aggressionCoef, 0.1, 1)
    M.shiftingAggression = shiftAggression
end


local function applyGearboxMode()
    local autoIndex = automaticHandling.modeIndexLookup[automaticHandling.mode]
    if autoIndex then
        automaticHandling.modeIndex = min(max(autoIndex, 1), #automaticHandling.modes)
        automaticHandling.mode = automaticHandling.modes[automaticHandling.modeIndex]
    end


    if automaticHandling.mode == "P" then
        gearbox:setMode("park")
    elseif automaticHandling.mode == "N" then
        gearbox:setMode("neutral")
    else
        gearbox:setMode("drive")
        local gearIndex = gearbox[dct.primaryAccess.gearIndexName]
        if automaticHandling.mode == "R" and gearbox[dct.access1.gearIndexName] > -1 then
            gearIndex = -1
            dct.primaryAccess = dct.access1
            dct.secondaryAccess = dct.access2
        elseif automaticHandling.mode ~= "R" and gearbox[dct.access1.gearIndexName] < 1 then
            gearIndex = 1
            dct.primaryAccess = dct.access1
            dct.secondaryAccess = dct.access2
        end


        if gearbox[dct.primaryAccess.gearIndexName] ~= gearIndex then
            newDesiredGearIndex = gearIndex
            previousGearIndex = gearbox[dct.primaryAccess.gearIndexName]
            M.timer.shiftDelayTimer = 0
            M.updateGearboxGFX = gearboxLogic.whileShifting
        end
    end


    M.isSportModeActive = automaticHandling.mode == "S"
    M.isManualModeActive = string.sub(automaticHandling.mode, 1, 1) == "M"
    if automaticHandling.mode ~= lastLoggedMode then
        lastLoggedMode = automaticHandling.mode
    end
end


local function setDefaultForwardMode(mode)
    automaticHandling.defaultForwardMode = mode
    if
        automaticHandling.mode == "D" or automaticHandling.mode == "S" or automaticHandling.mode == "1" or
            automaticHandling.mode == "2" or
            string.find(automaticHandling.mode, "M")
     then
        automaticHandling.mode = mode
        if mode == "M1" then
            automaticHandling.mode = "M" .. tostring(max(gearbox.gearIndex, 1))
        end
        applyGearboxMode()
        applyGearboxModeRestrictions()
    end
end


local function shiftUp()
    if automaticHandling.mode == "N" then
        M.timer.gearChangeDelayTimer = M.timerConstants.gearChangeDelay
    end


    local previousMode = automaticHandling.mode
    automaticHandling.modeIndex = min(automaticHandling.modeIndex + 1, #automaticHandling.modes)
    automaticHandling.mode = automaticHandling.modes[automaticHandling.modeIndex]


    if automaticHandling.mode == "M1" then
        automaticHandling.mode = "M" .. tostring(max(gearbox.gearIndex, 1))
    end


    applyGearboxMode()
    applyGearboxModeRestrictions()
end


local function shiftDown()
    if automaticHandling.mode == "N" then
        M.timer.gearChangeDelayTimer = M.timerConstants.gearChangeDelay
    end


    local previousMode = automaticHandling.mode
    automaticHandling.modeIndex = max(automaticHandling.modeIndex - 1, 1)
    automaticHandling.mode = automaticHandling.modes[automaticHandling.modeIndex]


    if previousMode == "M1" and electrics.values.wheelspeed > 2 and M.gearboxHandling.gearboxSafety then
        automaticHandling.mode = previousMode
    end


    applyGearboxMode()
    applyGearboxModeRestrictions()
end


local function shiftToGearIndex(index)
    local desiredMode = automaticHandling.hShifterModeLookup[index]
    if not desiredMode or not automaticHandling.existingModeLookup[desiredMode] then
        desiredMode = "N"
    end
    automaticHandling.mode = desiredMode


    if automaticHandling.mode == "M1" then
        automaticHandling.mode = "M" .. tostring(max(gearbox.gearIndex, 1))
    end


    applyGearboxMode()
    applyGearboxModeRestrictions()
end


local function dctPredictNextGear()
    local isEVMode = electrics.values.hybridMode == "ev"
    local currentAV =
        isEVMode and abs(gearbox.motorAV or gearbox.p2ShaftAV or (gearbox.outputAV1 * (gearbox.gearRatio or 1))) or
        engine.outputAV1


    local nextGear = gearbox[dct.secondaryAccess.gearIndexName]
    if
        M.throttle > 0 and gearbox[dct.primaryAccess.gearIndexName] > 0 and M.smoothedValues.brake <= 0 and
            (currentAV / max(M.shiftBehavior.shiftUpAV, 1)) > 0.85
     then
        nextGear = gearbox[dct.primaryAccess.gearIndexName] + sign(gearbox[dct.primaryAccess.gearIndexName])
    elseif gearbox[dct.primaryAccess.gearIndexName] > 0 then
        nextGear = gearbox[dct.primaryAccess.gearIndexName] - sign(gearbox[dct.primaryAccess.gearIndexName])
    end


    local maxGearIndexForCurrentSecondary =
        dct.secondaryAccess == dct.access1 and gearbox.maxGearIndex or gearbox.maxGearIndex - 1
    if
        gearbox[dct.secondaryAccess.gearIndexName] ~= nextGear and
            gearbox[dct.secondaryAccess.gearIndexName] < maxGearIndexForCurrentSecondary and
            M.timer.shiftDelayTimer <= 0
     then
        gearbox[dct.secondaryAccess.setGearIndexName](gearbox, nextGear)
        M.timer.shiftDelayTimer = M.timerConstants.shiftDelay
    end
end


local function updateExposedData()
    local hybridMode = electrics.values.hybridMode or "sport"
    local isReverse = (automaticHandling.mode == "R")
    isEngineAllowed =
        (hybridMode == "sport" or hybridMode == "ice" or hybridMode == "off") and not isReverse and
        (automaticHandling.mode ~= "P")


    local vc = controller.getController("vehicleController")


    if not isEngineAllowed then
        if vc and vc.setEngineIgnition and vc.engineIgnition then
            vc.setEngineIgnition(false)
        end


        if engine then
            engine.ignitionCoef = 0
            engine.throttle = 0
            engine.starterEngaged = false
            engine.isStalled = false
        end


        M.engineThrottle = 0
        M.engineLoad = 0
        M.isEngineRunning = 0
        M.running = false
        M.rpm = 0
    else
        if vc and vc.setEngineIgnition and not vc.engineIgnition then
            vc.setEngineIgnition(true)
        end


        if engine then
            engine.ignitionCoef = 1
            engine.isStalled = false
            local isEngineRunning = engine.outputAV1 > (engine.idleAV * 0.75)
            if not isEngineRunning and electrics.values.ignitionLevel > 1 then
                engine.starterEngaged = true
                if vc and vc.setStarter then
                    vc.setStarter(true)
                end
            else
                engine.starterEngaged = false
                if vc and vc.setStarter then
                    vc.setStarter(false)
                end
            end
        end


        local isIgnitionOn = (electrics.values.ignitionLevel or 0) > 1
        electrics.values.driverThrottle = isIgnitionOn and (M.inputValues.throttle or 0) or 0
        if not isIgnitionOn then
            M.engineThrottle = 0
        end
        M.rpm = engine and (engine.outputAV1 * constants.avToRPM) or 0
        M.engineLoad = engine and (engine.isDisabled and 0 or engine.instantEngineLoad) or 0
        M.engineTorque = engine and engine.combustionTorque or 0
        M.flywheelTorque = engine and engine.outputTorque1 or 0
        M.isEngineRunning = (engine and engine.outputAV1 > engine.idleAV * 0.5) and 1 or 0
        M.running = isEngineAllowed
    end


    if hybridMode ~= lastLoggedHybridMode then
        lastLoggedHybridMode = hybridMode
    end


    wasEngineAllowed = isEngineAllowed


    M.smoothedAvgAVInput = sharedFunctions.updateAvgAVSingleDevice("gearbox")
    M.waterTemp =
        (engine and engine.thermals) and (engine.thermals.coolantTemperature or engine.thermals.oilTemperature) or 0
    M.oilTemp = (engine and engine.thermals) and engine.thermals.oilTemperature or 0
    M.checkEngine = engine and engine.isDisabled or false
    M.ignition = electrics.values.ignitionLevel > 1
    M.gearboxTorque = gearbox and gearbox.outputTorque1 or 0
    M.minGearIndex = gearbox.minGearIndex
    M.maxGearIndex = gearbox.maxGearIndex
    electrics.values.driverThrottle = M.inputValues.throttle
end


local function updateInGear(dt)
    ctrlDebugTimer = ctrlDebugTimer + dt
    if ctrlDebugTimer >= 1.0 then
        ctrlDebugTimer = 0
        local rawDrvrThrot = electrics.values.driverThrottle or 0
        local engRPM = engine and (engine.outputAV1 * constants.avToRPM) or 0
        local motRPM = abs(gearbox.motorAV or gearbox.p2ShaftAV or 0) * constants.avToRPM
    end


    if lastShiftState ~= "inGear" then
        lastShiftState = "inGear"
    end
    local isIgnitionOn = (electrics.values.ignitionLevel or 0) > 1
    local rawPedal = isIgnitionOn and (M.inputValues.throttle or 0) or 0


    electrics.values.driverThrottle = rawPedal
    M.throttle = (isIgnitionOn and isEngineAllowed) and rawPedal or 0
    M.brake = M.inputValues.brake
    M.isArcadeSwitched = false
    M.isShifting = false


    local gearIndex = gearbox[dct.primaryAccess.gearIndexName]
    local transmissionAV = abs(gearbox.outputAV1 or 0) * abs(gearbox.gearRatios[gearIndex] or 1)


    local isEngineRunningAndLocked =
        isEngineAllowed and ((gearbox.k0ClutchRatio or 0) > 0.8) and
        (engine and engine.outputAV1 > (engine.idleAV * 0.75))
    local activeAV =
        isEngineRunningAndLocked and engine.outputAV1 or
        max(abs(gearbox.motorAV or gearbox.p2ShaftAV or 0), transmissionAV)


    local maxAV =
        isEngineAllowed and (engine and engine.maxAV or 6500 * constants.rpmToAV) or
        ((gearbox.p2MaxRPM or 7400) * constants.rpmToAV)
    if M.timer.gearChangeDelayTimer <= 0 and (not isEngineAllowed or (gearbox.clutchRatio or 0) >= 0.8) then
        local tmpAV = activeAV
        local relAV = tmpAV / abs(gearbox.gearRatios[gearIndex] or 1)
        local pedal = electrics.values.driverThrottle or 0


        local upshiftAV = isEngineAllowed and M.shiftBehavior.shiftUpAV or (maxAV * (0.45 + 0.45 * pedal))
        local downshiftAV = isEngineAllowed and M.shiftBehavior.shiftDownAV or (maxAV * 0.25)


        if not isEngineAllowed then
            if
                tmpAV >= upshiftAV and gearIndex > 0 and gearIndex < gearbox.maxGearIndex and M.brake <= 0 and
                    automaticHandling.mode ~= "N"
             then
                gearIndex = gearIndex + 1
            elseif tmpAV < downshiftAV and gearIndex > 1 and (pedal > 0.3 or M.brake > 0) then
                gearIndex = gearIndex - 1
            end
        else
            sharedFunctions.selectShiftPoints(gearIndex)
            local wheelSlipCanShiftDown = M.shiftPreventionData.wheelSlipShiftDown or M.brake <= 0
            while tmpAV < M.shiftBehavior.shiftDownAV and abs(gearIndex) > 1 and wheelSlipCanShiftDown do
                gearIndex = gearIndex - sign(gearIndex)
                tmpAV = relAV * (gearbox.gearRatios[gearIndex] or 0)
                if tmpAV > maxAV then
                    gearIndex = gearIndex + sign(gearIndex)
                    break
                end
                sharedFunctions.selectShiftPoints(gearIndex)
            end


            local wheelSlipCanShiftUp = M.shiftPreventionData.wheelSlipShiftUp or M.throttle <= 0
            if
                (tmpAV >= M.shiftBehavior.shiftUpAV or engine.revLimiterActive) and M.brake <= 0 and wheelSlipCanShiftUp and
                    gearIndex < gearbox.maxGearIndex and
                    gearIndex > 0
             then
                gearIndex = gearIndex + 1
            end
        end
    end


    local isManualMode = string.sub(automaticHandling.mode, 1, 1) == "M"
    gearIndex = min(max(gearIndex, automaticHandling.minGearIndex), automaticHandling.maxGearIndex)


    if gearbox[dct.primaryAccess.gearIndexName] ~= gearIndex then
        newDesiredGearIndex = gearIndex
        previousGearIndex = gearbox[dct.primaryAccess.gearIndexName]
        M.timer.shiftDelayTimer = 0
        calculateShiftAggression()
        M.updateGearboxGFX = gearboxLogic.whileShifting
    end


    local dctClutchRatio = 0
    local isParkOrNeutral = (automaticHandling.mode == "P" or automaticHandling.mode == "N" or abs(gearIndex) < 1)


    if isParkOrNeutral then
        dctClutchRatio = 0
        clutchHandling.clutchLaunchIFactor = 0
    else
        local isHoldingBrakeAtStandstill =
            (M.brake > 0.2) and (electrics.values.wheelspeed < 1.5) and (M.throttle <= 0.05)


        if isHoldingBrakeAtStandstill then
            dctClutchRatio = 0
            clutchHandling.clutchLaunchIFactor = 0
        else
            if
                isEngineAllowed and abs(gearIndex) == 1 and (gearbox.k0ClutchRatio or 0) > 0.8 and
                    (gearbox.p2MotorTorque or 0) <= 0
             then
                local engineAV = engine.outputAV1
                local ratio = min(max((engineAV - engine.idleAV) / max(engine.idleAV * 0.8, 1), 0), 1)
                dctClutchRatio = min(max(ratio, 0.25), 1)
            else
                dctClutchRatio = 1
            end
        end
    end
    if (M.throttle > 0.5 and M.brake > 0.5 and electrics.values.wheelspeed < 2) then
        dctClutchRatio = 0
        clutchHandling.isClutchOpenLaunch = true
    end


    electrics.values[dct.primaryAccess.clutchRatioName] = dctClutchRatio
    electrics.values[dct.secondaryAccess.clutchRatioName] = 0
    M.clutchRatio = dctClutchRatio
    M.currentGearIndex = (automaticHandling.mode == "N" or automaticHandling.mode == "P") and 0 or gearIndex
    gearbox.gearIndex = gearbox[dct.primaryAccess.gearIndexName]
    gearbox.gearRatio = gearbox.gearRatios[gearbox[dct.primaryAccess.gearIndexName]]


    if isManualMode then
        automaticHandling.mode = "M" .. gearIndex
        automaticHandling.modeIndex = automaticHandling.modeIndexLookup[automaticHandling.mode]
        applyGearboxModeRestrictions()
    end


    updateExposedData()
    dctPredictNextGear()
end


local function updateWhileShifting(dt)
    if lastShiftState ~= "whileShifting" then
        lastShiftState = "whileShifting"
    end


    local isIgnitionOn = (electrics.values.ignitionLevel or 0) > 1
    local rawPedal = isIgnitionOn and (M.inputValues.throttle or 0) or 0


    electrics.values.driverThrottle = rawPedal
    M.throttle =
        (isIgnitionOn and isEngineAllowed and newDesiredGearIndex > gearbox[dct.primaryAccess.gearIndexName]) and
        rawPedal or
        0
    M.brake = M.inputValues.brake
    M.isArcadeSwitched = false
    M.isShifting = true
    M.currentGearIndex = newDesiredGearIndex


    local primaryGearIndex = gearbox[dct.primaryAccess.gearIndexName]
    local secondaryGearIndex = gearbox[dct.secondaryAccess.gearIndexName]


    if newDesiredGearIndex ~= secondaryGearIndex and M.timer.shiftDelayTimer <= 0 then
        local sameShaft = newDesiredGearIndex % 2 == secondaryGearIndex % 2
        local newGearIndex =
            sameShaft and newDesiredGearIndex or primaryGearIndex + sign(newDesiredGearIndex - primaryGearIndex)
        if secondaryGearIndex ~= newGearIndex then
            gearbox[dct.secondaryAccess.setGearIndexName](gearbox, newGearIndex)
            M.timer.shiftDelayTimer = M.timerConstants.shiftDelay * 0.5
        end
    end


    local canShift = true
    if isEngineAllowed then
        local isEngineRunning = engine.ignitionCoef >= 1 and not engine.isStalled
        local targetGearRatio = gearbox.gearRatios[newDesiredGearIndex]
        local targetAV = targetGearRatio * gearbox.outputAV1
        local isDownShift = abs(newDesiredGearIndex) < abs(primaryGearIndex)
        if isDownShift and targetAV > engine.outputAV1 and not clutchHandling.didRevMatch and isEngineRunning then
            M.throttle = clutchHandling.revMatchThrottle
            electrics.values[dct.primaryAccess.clutchRatioName] = 0
            electrics.values[dct.secondaryAccess.clutchRatioName] = 0
            M.timer.revMatchTimer = M.timer.revMatchTimer + dt
            local revMatchExpired = M.timer.revMatchTimer > M.timerConstants.revMatchExpired
            canShift =
                (engine.outputAV1 >= (targetAV - (500 * constants.rpmToAV))) or targetAV > engine.maxAV or
                revMatchExpired
            if canShift then
                M.throttle = 0
            end
            clutchHandling.didRevMatch = canShift
        elseif not clutchHandling.didRevMatch then
            clutchHandling.didRevMatch = true
        end
    else
        canShift = true
        clutchHandling.didRevMatch = true
    end


    local adjustedClutchTime =
        clamp(
        (dct.clutchTime - dct.clutchTime * 10) * shiftAggression + dct.clutchTime * 10,
        dct.clutchTime,
        dct.clutchTime * 10
    )
    if M.timer.shiftDelayTimer <= 0 and canShift then
        if
            isEngineAllowed and gearbox[dct.primaryAccess.gearIndexName] > 0 and
                gearbox[dct.primaryAccess.gearIndexName] < gearbox[dct.secondaryAccess.gearIndexName] and
                M.throttle > 0 and
                adjustedClutchTime < 0.15 and
                not clutchHandling.didCutIgnition
         then
            engine:cutIgnition(adjustedClutchTime)
            clutchHandling.didCutIgnition = true
        end


        local clutchRatio =
            min(electrics.values[dct.secondaryAccess.clutchRatioName] + (1 / adjustedClutchTime) * dt, 1)
        local stallPrevent = 1
        if isEngineAllowed then
            stallPrevent = min(max((engine.outputAV1 * 0.9 - engine.idleAV) / (engine.idleAV * 0.1), 0), 1)
        end


        local isDownShift = abs(newDesiredGearIndex) < abs(primaryGearIndex)
        electrics.values[dct.primaryAccess.clutchRatioName] =
            isDownShift and 0 or min(1 - clutchRatio, stallPrevent * stallPrevent)
        electrics.values[dct.secondaryAccess.clutchRatioName] = min(clutchRatio, stallPrevent * stallPrevent)


        if clutchRatio == 1 or (isEngineAllowed and stallPrevent < 1) then
            dct.primaryAccess, dct.secondaryAccess = dct.secondaryAccess, dct.primaryAccess
            primaryGearIndex = gearbox[dct.primaryAccess.gearIndexName]
            clutchHandling.didRevMatch = false
            clutchHandling.didCutIgnition = false


            if newDesiredGearIndex == primaryGearIndex then
                M.updateGearboxGFX = gearboxLogic.inGear
                M.timer.gearChangeDelayTimer = M.timerConstants.gearChangeDelay
                M.timer.revMatchTimer = 0
            end
        end
    end


    if newDesiredGearIndex * gearbox[dct.primaryAccess.gearIndexName] <= 0 then
        electrics.values[dct.primaryAccess.clutchRatioName] = 0
    end


    gearbox.gearIndex = newDesiredGearIndex
    updateExposedData()
end


local function updateInGearArcade(dt)
    local wheelSpeed = electrics.values.wheelspeed or 0
    local rawThrottle = M.inputValues.throttle or 0
    local rawBrake = M.inputValues.brake or 0
    if automaticHandling.mode == "R" then
        if wheelSpeed < 0.8 and rawThrottle > 0 and rawBrake <= 0 then
            automaticHandling.mode = automaticHandling.defaultForwardMode
            applyGearboxMode()
            applyGearboxModeRestrictions()
        end
    else
        if wheelSpeed < 0.8 and rawBrake > 0 and rawThrottle <= 0 then
            automaticHandling.mode = "R"
            applyGearboxMode()
            applyGearboxModeRestrictions()
        end
    end
    if automaticHandling.mode == "R" then
        M.isArcadeSwitched = true
        M.inputValues.throttle = rawBrake
        M.inputValues.brake = rawThrottle
    else
        M.isArcadeSwitched = false
        M.inputValues.throttle = rawThrottle
        M.inputValues.brake = rawBrake
    end


    updateInGear(dt)


    M.inputValues.throttle = rawThrottle
    M.inputValues.brake = rawBrake
end


local function updateWhileShiftingArcade(dt)
    local rawThrottle = M.inputValues.throttle or 0
    local rawBrake = M.inputValues.brake or 0


    if automaticHandling.mode == "R" then
        M.isArcadeSwitched = true
        M.inputValues.throttle = rawBrake
        M.inputValues.brake = rawThrottle
    else
        M.isArcadeSwitched = false
        M.inputValues.throttle = rawThrottle
        M.inputValues.brake = rawBrake
    end


    updateWhileShifting(dt)
    M.inputValues.throttle = rawThrottle
    M.inputValues.brake = rawBrake
end


local function sendTorqueData()
    if engine then
        engine:sendTorqueData()
    end
end


local function init(jbeamData, sharedFunctionTable)
    print("[P2-CTRL] INIT: p2DCTGearboxLogic loaded!")
    sharedFunctions = sharedFunctionTable
    engine = powertrain.getDevice("mainEngine")
    gearbox = powertrain.getDevice("gearbox")
    newDesiredGearIndex = 0
    previousGearIndex = 0


    M.currentGearIndex = 0
    M.maxGearIndex = 0
    M.minGearIndex = 0
    M.throttle = 0
    M.brake = 0
    M.clutchRatio = 0
    M.shiftingAggression = 0
    M.isArcadeSwitched = false
    M.isSportModeActive = false
    M.isManualModeActive = false


    M.smoothedAvgAVInput = 0
    M.rpm = 0
    M.idleRPM = 0
    M.maxRPM = 0


    M.engineThrottle = 0
    M.engineLoad = 0
    M.engineTorque = 0
    M.flywheelTorque = 0
    M.gearboxTorque = 0


    M.ignition = true
    M.isEngineRunning = 0


    M.oilTemp = 0
    M.waterTemp = 0
    M.checkEngine = false


    gearboxAvailableLogic = {
        arcade = {
            inGear = updateInGearArcade,
            whileShifting = updateWhileShiftingArcade,
            shiftUp = sharedFunctions.warnCannotShiftSequential,
            shiftDown = sharedFunctions.warnCannotShiftSequential,
            shiftToGearIndex = sharedFunctions.switchToRealisticBehavior
        },
        realistic = {
            inGear = updateInGear,
            whileShifting = updateWhileShifting,
            shiftUp = shiftUp,
            shiftDown = shiftDown,
            shiftToGearIndex = shiftToGearIndex
        }
    }


    clutchHandling.didRevMatch = false
    clutchHandling.didCutIgnition = false
    clutchHandling.isClutchOpenLaunch = false
    clutchHandling.clutchLaunchTargetAV = (jbeamData.clutchLaunchTargetRPM or 3000) * constants.rpmToAV * 0.5
    clutchHandling.clutchLaunchStartAV =
        ((jbeamData.clutchLaunchStartRPM or 2000) * constants.rpmToAV - engine.idleAV) * 0.5
    clutchHandling.clutchLaunchIFactor = 0
    clutchHandling.revMatchThrottle = jbeamData.revMatchThrottle or 0.5


    automaticHandling.availableModeLookup = {}
    for _, v in pairs(automaticHandling.availableModes) do
        automaticHandling.availableModeLookup[v] = true
    end


    automaticHandling.modes = {}
    automaticHandling.modeIndexLookup = {}
    local modes = jbeamData.automaticModes or "PRNDSM"
    local modeCount = #modes
    local modeOffset = 0
    for i = 1, modeCount do
        local mode = modes:sub(i, i)
        if automaticHandling.availableModeLookup[mode] then
            if mode ~= "M" then
                automaticHandling.modes[i + modeOffset] = mode
                automaticHandling.modeIndexLookup[mode] = i + modeOffset
                automaticHandling.existingModeLookup[mode] = true
            else
                for j = 1, gearbox.maxGearIndex, 1 do
                    local manualMode = "M" .. tostring(j)
                    local manualModeIndex = i + j - 1
                    automaticHandling.modes[manualModeIndex] = manualMode
                    automaticHandling.modeIndexLookup[manualMode] = manualModeIndex
                    automaticHandling.existingModeLookup[manualMode] = true
                    modeOffset = j - 1
                end
            end
        end
    end


    local defaultMode = jbeamData.defaultAutomaticMode or "N"
    automaticHandling.modeIndex = string.find(modes, defaultMode) or 1
    automaticHandling.mode = automaticHandling.modes[automaticHandling.modeIndex]
    automaticHandling.maxGearIndex = gearbox.maxGearIndex
    automaticHandling.minGearIndex = gearbox.minGearIndex
    automaticHandling.autoDownShiftInM = jbeamData.autoDownShiftInM == nil and true or jbeamData.autoDownShiftInM


    dct.clutchTime = jbeamData.dctClutchTime or 0.05


    M.maxRPM = engine.maxRPM
    M.idleRPM = engine.idleRPM
    M.maxGearIndex = automaticHandling.maxGearIndex
    M.minGearIndex = abs(automaticHandling.minGearIndex)


    local storages = sharedFunctions.getEnergyStorages({engine}) or {}
    local batteryName = gearbox.p2BatteryStorageName or "mainBattery"
    local batteryStorage = energyStorage.getStorage(batteryName)


    if batteryStorage then
        table.insert(storages, batteryStorage)
        print(
            string.format(
                "[P2-CTRL] Registered Hybrid Battery '%s' (%.2f kWh) to vehicleController",
                batteryName,
                (batteryStorage.energyCapacity or 0) / 3600000
            )
        )
    else
        for name, s in pairs(energyStorage.getStorages() or {}) do
            if s.type == "electricBattery" or s.energyType == "electricEnergy" then
                table.insert(storages, s)
                print(
                    string.format(
                        "[P2-CTRL] Auto-detected vehicle battery '%s' (%.2f kWh) and registered to vehicleController",
                        name,
                        (s.energyCapacity or 0) / 3600000
                    )
                )
                break
            end
        end
    end


    M.energyStorages = storages


    dct.primaryAccess = dct.access1
    dct.secondaryAccess = dct.access2


    local defaultForwardMode = jbeamData.defaultForwardMode or "D"
    setDefaultForwardMode(defaultForwardMode)
    applyGearboxMode()
end


local function getState()
    local data = {grb_mde = automaticHandling.mode}
    return tableIsEmpty(data) and nil or data
end


local function setState(data)
    if data.grb_mde then
        automaticHandling.mode = data.grb_mde
        automaticHandling.modeIndex = automaticHandling.modeIndexLookup[automaticHandling.mode]
        applyGearboxMode()
        applyGearboxModeRestrictions()
    end
end


M.init = init
M.gearboxBehaviorChanged = gearboxBehaviorChanged
M.shiftUp = shiftUp
M.shiftDown = shiftDown
M.shiftToGearIndex = shiftToGearIndex
M.updateGearboxGFX = nop
M.getGearName = getGearName
M.getGearPosition = getGearPosition
M.setDefaultForwardMode = setDefaultForwardMode
M.sendTorqueData = sendTorqueData
M.areShiftLEDsInUse = areShiftLEDsInUse
M.getState = getState
M.setState = setState


return M




p2DCTGearbox.lua

local M = {}


M.outputPorts = {[1] = true}
M.deviceCategories = {clutchlike = true, gearbox = true}
M.requiredExternalInertiaOutputs = {1}


local max = math.max
local min = math.min
local abs = math.abs
local sqrt = math.sqrt


local rpmToAV = 0.104719755
local avToRPM = 9.549296596425384


local gbxDebugTimer = 0
local lastLoggedK0 = nil
local lastLoggedG1 = nil
local lastLoggedG2 = nil


local function updateSounds(device, dt)
  local isVehicleOn = (electrics.values.ignitionLevel or 0) >= 1
  local hybridMode = electrics.values.hybridMode or "sport"
  local isReverse = (device.gearIndex1 and device.gearIndex1 < 0) or (electrics.values.gear == "R")
  local isEVMode = (hybridMode == "ev") or isReverse  local outputRPM = abs(device.gearWhineOutputAV) * avToRPM
  local inputRPM = abs(device.p2ShaftAV) * avToRPM  local pitchInput = clamp(inputRPM * (device.forwardInputPitchCoef or 1.0), 0, 20000)
  local pitchOutput = clamp(outputRPM * (device.forwardOutputPitchCoef or 1.0), 0, 20000)  local motorLoad = clamp(abs(device.p2MotorTorque or 0) / max(device.p2MaxTorque, 1), 0, 1)  local p2Volume = 0
  local p2Pitch = 0


  if isVehicleOn then
    if isEVMode then
      local baseIdleHumVolume = 5
      local motionVolume = clamp(inputRPM / 2000, 0, 1) * 80
      local loadVolume = motorLoad * 12


      p2Volume = clamp(baseIdleHumVolume + motionVolume + loadVolume, 0, 100) * device.p2Volume      if inputRPM < 1000 then
        p2Pitch = 10000 + (motorLoad * 1000)
      else
        p2Pitch = clamp(pitchInput * 0.85 + 2000, 2000, 20000)
      end
    else


      if inputRPM > 200 and motorLoad > 0.05 then
        p2Volume = clamp((motorLoad * 1.5) + (inputRPM / 4000) * 0.5, 0, 100) * device.p2Volume
        p2Pitch = pitchInput
      end
    end
  end


  local outputRPMSign = sign(device.gearWhineOutputAV)
  if outputRPMSign == 0 then outputRPMSign = 1 end  if device.p2SoundLoop then
    device.p2SoundLoop:setVolumePitch(p2Volume, p2Pitch, motorLoad, outputRPMSign)
  end  local volumeInput = 0
  local volumeOutput = 0
  if isVehicleOn and inputRPM > 150 then
    volumeInput = clamp(10 + (motorLoad * 24) + (inputRPM / 6000) * 60, 0, 50) * device.gearVolume
    volumeOutput = clamp(8 + (motorLoad * 20) + (outputRPM / 4000) * 60, 0, 40) * device.gearVolume
  end


  if device.gearWhineOutputLoop then
    device.gearWhineOutputLoop:setVolumePitch(volumeOutput, pitchOutput, motorLoad, outputRPMSign)
  end
  if device.gearWhineInputLoop then
    device.gearWhineInputLoop:setVolumePitch(volumeInput, pitchInput, motorLoad, outputRPMSign)
  end
end


local function updateVelocity(device, dt)
  device.clutchAV1 = (device.outputAV1 or 0) * device.gearRatio1 * device.lockCoef
  device.clutchAV2 = (device.outputAV1 or 0) * device.gearRatio2 * device.lockCoef


  local parentAV = (device.parent and device.parent.outputAV1) or 0  local c1 = device.clutchRatio1 or 0
  local c2 = device.clutchRatio2 or 0
  local transAV = 0
  if c1 > 0 or c2 > 0 then
    transAV = (device.clutchAV1 * c1 + device.clutchAV2 * c2) / max(c1 + c2, 0.001)
  end


  local k0 = device.k0ClutchRatio or 0
  if k0 >= 0.99 then


    device.inputAV = parentAV
    device.p2ShaftAV = device.inputAV
  elseif k0 <= 0.01 then


    device.p2ShaftAV = transAV
    device.inputAV = parentAV
  else


    device.p2ShaftAV = transAV * (1 - k0) + parentAV * k0
    device.inputAV = parentAV
  end


  device.motorAV = device.p2ShaftAV
  device.motorRPM = abs(device.p2ShaftAV) * avToRPM
  electrics.values.motorRPM = device.motorRPM
  electrics.values.motorAV = device.motorAV
end


local function neutralUpdateVelocity(device, dt)
  device.inputAV = (device.parent and device.parent.outputAV1) or 0
  device.clutchAV1 = (device.outputAV1 or 0) * device.gearRatio1 * device.lockCoef
  device.clutchAV2 = (device.outputAV1 or 0) * device.gearRatio2 * device.lockCoef
  device.p2ShaftAV = device.inputAV
end


local function updateTorque(device, dt)
  local isVehicleOn = (electrics.values.ignitionLevel or 0) > 1
  local hybridMode = electrics.values.hybridMode or "sport"


  local rawThrottle = electrics.values.driverThrottle or electrics.values.throttle_input or electrics.values.throttle or 0
  local throttle = isVehicleOn and rawThrottle or 0
  local brake = isVehicleOn and (electrics.values.brake or 0) or 0
local battery = energyStorage.getStorage(device.p2BatteryStorageName)
if not battery then
  for name, s in pairs(energyStorage.getStorages() or {}) do
    if s.type == "electricBattery" or s.energyType == "electricEnergy" then
      device.p2BatteryStorageName = name
      battery = s
      break
    end
  end
end


local hasBatteryPower = isVehicleOn
if battery and battery.storedEnergy and battery.storedEnergy <= 0 then
  hasBatteryPower = false
end


  local isReverse = (device.gearIndex1 and device.gearIndex1 < 0) or (electrics.values.gear == "R")
  local isEVMode = (hybridMode == "ev") or isReverse
  local parentAV = (device.parent and device.parent.outputAV1) or 0
  local parentIdleAV = (device.parent and device.parent.idleAV) or (800 * rpmToAV)
local targetK0 = 0
local eBoostScale = 1.0


if isEVMode then
  targetK0 = 0
  eBoostScale = 1.0
elseif hybridMode == "sport" then


  eBoostScale = 1.0
  targetK0 = (parentAV > parentIdleAV * 0.75) and 1.0 or 0.0
elseif hybridMode == "ice" or hybridMode == "sport_ice" then


  eBoostScale = 0.0
  targetK0 = (parentAV > parentIdleAV * 0.75) and 1.0 or 0.0
elseif hybridMode == "off" then
  eBoostScale = 0.35
  targetK0 = (parentAV > parentIdleAV * 0.75) and 1.0 or 0.0
else
  eBoostScale = 0.75
  targetK0 = (parentAV > parentIdleAV * 0.75) and 1.0 or 0.0
end


  if device.k0ClutchRatio < targetK0 then
    device.k0ClutchRatio = min(device.k0ClutchRatio + dt * 4.0, targetK0)
  elseif device.k0ClutchRatio > targetK0 then
    device.k0ClutchRatio = max(device.k0ClutchRatio - dt * 8.0, targetK0)
  end  local p2RPM = abs(device.p2ShaftAV) * avToRPM
  local availableTorque = device.p2MaxTorque or 280


  if p2RPM > 1000 and (device.p2MaxPower) then
    local powerLimitedTorque = (device.p2MaxPower) / max(abs(device.p2ShaftAV), 1)
    availableTorque = min(availableTorque, powerLimitedTorque)
  end


  if p2RPM >= (device.p2MaxRPM or 8000) or not hasBatteryPower or not isVehicleOn then
    availableTorque = 0
  end  device.isRegenActive = false
  local isABSActive = (electrics.values.abs or 0) > 0.1 or (electrics.values.absActive == true)


  if throttle > 0 and availableTorque > 0 then
    device.p2MotorTorque = throttle * availableTorque * eBoostScale
  elseif brake > 0 and battery and not isReverse and p2RPM > 400 and abs(device.gearIndex1 or 1) >= 1 and not isABSActive then


    local speedTaper = min(max((p2RPM - 400) / 600, 0), 1)
    local regenStrength = (hybridMode == "ev" or hybridMode == "sport") and 0.55 or 0.30
    local regenTorque = brake * (device.p2MaxRegenTorque or 140) * regenStrength * speedTaper


    device.p2MotorTorque = -regenTorque
    device.isRegenActive = true
  else
    device.p2MotorTorque = 0
  endif battery and battery.energyCapacity and battery.energyCapacity > 0 then


  local mechanicalPower = abs(device.p2MotorTorque) * abs(device.p2ShaftAV)


  if device.isRegenActive then


    local energyHarvested = mechanicalPower * dt * (device.p2RegenEfficiency or 0.85)
    battery.storedEnergy = min(battery.storedEnergy + energyHarvested, battery.energyCapacity)
  elseif device.p2MotorTorque > 0 then


    local energySpent = (mechanicalPower * dt) / (device.p2DriveEfficiency or 0.90)
    battery.storedEnergy = max(battery.storedEnergy - energySpent, 0)
  end


  battery.remainingRatio = battery.storedEnergy / battery.energyCapacity  electrics.values.batteryLevel = battery.remainingRatio
  electrics.values.batteryCharge = battery.storedEnergy
  electrics.values.batteryCapacity = battery.energyCapacity  if isEVMode then
    electrics.values.fuel = battery.remainingRatio
    electrics.values.fuelCapacity = battery.energyCapacity
    electrics.values.fuelVolume = battery.storedEnergy
  end
end  device.clutchRatio1 = electrics.values[device.electricsClutchRatio1Name] or 0
  device.clutchRatio2 = electrics.values[device.electricsClutchRatio2Name] or 0


  if abs(device.gearIndex1 or 0) >= 1 and throttle > 0.05 and (device.clutchRatio1 + device.clutchRatio2) < 0.1 then
    device.clutchRatio1 = 1
  end


  device.clutchRatio = max(device.clutchRatio1, device.clutchRatio2)  local avDiff1 = device.p2ShaftAV - device.clutchAV1
  local avDiff2 = device.p2ShaftAV - device.clutchAV2
  local clutchRatio1 = device.clutchRatio1
  local clutchRatio2 = device.clutchRatio2
  local maxClutchAngle1 = device.maxClutchAngle1
  local maxClutchAngle2 = device.maxClutchAngle2


  device.clutchAngle1 = min(max(device.clutchAngle1 + avDiff1 * dt * device.clutchStiffness, -maxClutchAngle1 * clutchRatio1), maxClutchAngle1 * clutchRatio1)
  device.clutchAngle2 = min(max(device.clutchAngle2 + avDiff2 * dt * device.clutchStiffness, -maxClutchAngle2 * clutchRatio2), maxClutchAngle2 * clutchRatio2)


  local lockTorque = device.lockTorque * device.wearLockTorqueCoef * device.damageLockTorqueCoef
  device.torqueDiff1 = min(max(device.clutchAngle1 * device.lockSpring1 + device.lockDamp1 * avDiff1 * clutchRatio1, -lockTorque), lockTorque)
  device.torqueDiff2 = min(max(device.clutchAngle2 * device.lockSpring2 + device.lockDamp2 * avDiff2 * clutchRatio2, -lockTorque), lockTorque)  local activeGearRatio1 = device.gearRatio1
  local activeGearRatio2 = device.gearRatio2


  local engineTorqueToOutput = ((device.torqueDiff1 - device.friction * min(max(device.clutchAV1, -1), 1) * clutchRatio1) * activeGearRatio1
                             + (device.torqueDiff2 - device.friction * min(max(device.clutchAV2, -1), 1) * clutchRatio2) * activeGearRatio2) * device.k0ClutchRatio


  local totalEngagedClutch = clutchRatio1 + clutchRatio2
  local motorTorqueToOutput = 0
  if totalEngagedClutch > 0 then
    motorTorqueToOutput = device.p2MotorTorque * ((clutchRatio1 * activeGearRatio1) + (clutchRatio2 * activeGearRatio2)) / totalEngagedClutch
  end


  device.outputTorque1 = (engineTorqueToOutput + motorTorqueToOutput) * device.lockCoef


  if isEVMode or device.k0ClutchRatio <= 0.05 then
    device.torqueDiff = 0
  else
    device.torqueDiff = (device.torqueDiff1 + device.torqueDiff2) * device.k0ClutchRatio
  end  electrics.values.p2MotorTorque = device.p2MotorTorque
  electrics.values.k0ClutchRatio = device.k0ClutchRatio
  electrics.values.eBoostScale = eBoostScale


  local totalDisplayTorque = device.torqueDiff1 + device.torqueDiff2 + device.p2MotorTorque
  device.gearWhineInputTorque = device.gearWhineInputTorqueSmoother:get(totalDisplayTorque)
  device.gearWhineOutputTorque = device.gearWhineOutputTorqueSmoother:get(device.outputTorque1)
  device.gearWhineInputAV = device.gearWhineInputAVSmoother:get(device.p2ShaftAV)
  device.gearWhineOutputAV = device.gearWhineOutputAVSmoother:get(device.outputAV1)
end


local function neutralUpdateTorque(device, dt)
  device.torqueDiff = 0
  device.outputTorque1 = 0


  device.gearWhineInputTorque = device.gearWhineInputTorqueSmoother:get(device.torqueDiff)
  device.gearWhineOutputTorque = device.gearWhineOutputTorqueSmoother:get(device.outputTorque1)
  device.gearWhineInputAV = device.gearWhineInputAVSmoother:get(max(device.clutchAV1, device.clutchAV2))
  device.gearWhineOutputAV = device.gearWhineOutputAVSmoother:get(device.outputAV1)
end


local function parkUpdateVelocity(device, dt)
  device.inputAV = device.parent.outputAV1
  device.clutchAV1 = device.outputAV1 * device.gearRatio1 * device.lockCoef
  device.clutchAV2 = device.outputAV1 * device.gearRatio2 * device.lockCoef
end


local function parkUpdateTorque(device, dt)
  device.torqueDiff = 0
  local outputAV1 = device.outputAV1


  if abs(outputAV1) < 100 then
    device.parkEngaged = 1
  end


  device.parkClutchAngle = min(max(device.parkClutchAngle + outputAV1 * dt, -device.maxParkClutchAngle), device.maxParkClutchAngle)
  device.outputTorque1 = -(device.parkClutchAngle * device.parkLockSpring + device.parkLockDamp * outputAV1) * device.parkEngaged


  device.gearWhineInputTorque = device.gearWhineInputTorqueSmoother:get(device.torqueDiff)
  device.gearWhineOutputTorque = device.gearWhineOutputTorqueSmoother:get(device.outputTorque1)
  device.gearWhineInputAV = device.gearWhineInputAVSmoother:get(max(device.clutchAV1, device.clutchAV2))
  device.gearWhineOutputAV = device.gearWhineOutputAVSmoother:get(device.outputAV1)
end


local function selectUpdates(device)
  device.velocityUpdate = updateVelocity
  device.torqueUpdate = updateTorque


  if device.mode == "neutral" then
    device.velocityUpdate = neutralUpdateVelocity
    device.torqueUpdate = neutralUpdateTorque
    device.parkEngaged = 0
    device.parkClutchAngle = 0
  end


  if device.mode == "park" then
    device.velocityUpdate = parkUpdateVelocity
    device.torqueUpdate = parkUpdateTorque
    device.parkEngaged = 0
  end  if device.mode == "drive" then
    device.parkEngaged = 0
    device.parkClutchAngle = 0
    device.lockCoef = 1
  end
end


local function applyDeformGroupDamage(device, damageAmount)
  device.damageLockTorqueCoef = max(device.damageLockTorqueCoef - linearScale(damageAmount, 0, 0.01, 0, 0.1), 0.2)
end


local function setPartCondition(device, subSystem, odometer, integrity, visual)
  device.wearLockTorqueCoef = linearScale(odometer, 30000000, 500000000, 1, 0.7)
  local integrityState = integrity
  if type(integrity) == "number" then
    local integrityValue = integrity
    integrityState = {
      damageLockTorqueCoef = linearScale(integrityValue, 1, 0, 1, 0.5)
    }
  end


  device.damageLockTorqueCoef = integrityState.damageLockTorqueCoef or 1
end


local function getPartCondition(device)
  local integrityState = {
    damageLockTorqueCoef = device.damageLockTorqueCoef
  }
  local integrityValueLockTorque = linearScale(device.damageLockTorqueCoef, 1, 0.5, 1, 0)
  local integrityValue = min(integrityValueLockTorque)


  return integrityValue, integrityState
end


local function validate(device)
  if not device.parent.deviceCategories.engine then
    log("E", "dctGearbox.validate", "Parent device is not an engine device...")
    return false
  end


  device.lockTorque = device.lockTorque or (device.parent.torqueData.maxTorque * 1.25 + device.parent.maxRPM * device.parent.inertia * math.pi / 30)


  local maxEngineTorque
  local maxEngineAV


  if device.parent and device.parent.deviceCategories.engine then
    local engine = device.parent
    local torqueData = engine:getTorqueData()
    maxEngineTorque = torqueData.maxTorque
    maxEngineAV = engine.maxAV
  else
    maxEngineTorque = 100
    maxEngineAV = 6000 * rpmToAV
  end


  device.maxExpectedInputTorque = maxEngineTorque
  device.invMaxExpectedInputTorque = 1 / maxEngineTorque
  device.invMaxExpectedOutputTorque = 0
  device.maxExpectedPower = maxEngineAV * device.maxExpectedInputTorque
  device.invMaxExpectedPower = 1 / device.maxExpectedPower
  device.maxExpectedOutputAV = maxEngineAV / device.minGearRatio
  device.invMaxExpectedOutputAV = 1 / device.maxExpectedOutputAV
  device.invMaxExpectedInputAV = 1 / maxEngineAV
  return true
end


local function setMode(device, mode)
  device.mode = mode
  selectUpdates(device)
end


local function setGearIndex1(device, index)
  device.gearIndex1 = min(max(index, device.minGearIndex), device.maxGearIndex)
  device.gearRatio1 = device.gearRatios[device.gearIndex1]
  powertrain.calculateTreeInertia()
  selectUpdates(device)
end


local function setGearIndex2(device, index)
  device.gearIndex2 = min(max(index, device.minGearIndex), device.maxGearIndex)
  device.gearRatio2 = device.gearRatios[device.gearIndex2]
  powertrain.calculateTreeInertia()
  selectUpdates(device)
end


local function setLock(device, enabled)
  device.lockCoef = enabled and 0 or 1
end


local function calculateInertia(device)
  local outputInertia = 0
  local cumulativeGearRatio = 1
  local maxCumulativeGearRatio = 1
  if device.children and #device.children > 0 then
    local child = device.children[1]
    outputInertia = child.cumulativeInertia
    cumulativeGearRatio = child.cumulativeGearRatio
    maxCumulativeGearRatio = child.maxCumulativeGearRatio
  end


  local gearRatio1 = device.gearRatio1
  local gearRatio2 = device.gearRatio2
  local divisionSafeGearRatio1 = gearRatio1 ~= 0 and abs(gearRatio1) or (device.maxGearRatio * 2)
  local divisionSafeGearRatio2 = gearRatio2 ~= 0 and abs(gearRatio2) or (device.maxGearRatio * 2)


  device.cumulativeInertia1 = min(outputInertia / divisionSafeGearRatio1 / divisionSafeGearRatio1, device.parent.inertia * 0.5)
  device.cumulativeInertia2 = min(outputInertia / divisionSafeGearRatio2 / divisionSafeGearRatio2, device.parent.inertia * 0.5)


  device.lockSpring1 = device.lockSpringBase or (powertrain.stabilityCoef * powertrain.stabilityCoef * device.cumulativeInertia1)
  device.lockSpring2 = device.lockSpringBase or (powertrain.stabilityCoef * powertrain.stabilityCoef * device.cumulativeInertia2)


  device.lockDamp1 = device.lockDampRatio1 * sqrt(device.lockSpring1 * device.cumulativeInertia1)
  device.lockDamp2 = device.lockDampRatio2 * sqrt(device.lockSpring2 * device.cumulativeInertia2)


  device.maxClutchAngle1 = device.lockTorque / device.lockSpring1
  device.maxClutchAngle2 = device.lockTorque / device.lockSpring2


  device.parkLockSpring = device.parkLockSpringBase or (powertrain.stabilityCoef * powertrain.stabilityCoef * outputInertia * 0.5)
  device.parkLockDamp = device.parkLockDampRatio * sqrt(device.parkLockSpring * outputInertia)
  device.maxParkClutchAngle = device.parkLockTorque / device.parkLockSpring


  device.cumulativeGearRatio = cumulativeGearRatio * (device.clutchRatio1 > device.clutchRatio2 and gearRatio1 or gearRatio2)
  device.maxCumulativeGearRatio = maxCumulativeGearRatio * device.maxGearRatio
end


local function resetSounds(device)
  device.gearWhineInputTorqueSmoother:reset()
  device.gearWhineOutputTorqueSmoother:reset()
  device.gearWhineInputAVSmoother:reset()
  device.gearWhineOutputAVSmoother:reset()


  device.gearWhineInputAV = 0
  device.gearWhineOutputAV = 0
  device.gearWhineInputTorque = 0
  device.gearWhineOutputTorque = 0


  if device.p2SoundLoop then
    device.p2SoundLoop:setVolumePitch(0, 0, 0, 0)
  end
end


local function initSounds(device, jbeamData)
  local gearWhineOutputSample = jbeamData.gearWhineOutputEvent or "event:>Vehicle>Transmission>helical_01>twine_out"
  local gearWhineInputSample = jbeamData.gearWhineInputEvent or "event:>Vehicle>Transmission>helical_01>twine_in"


  device.gearWhineOutputLoop = sounds.createSoundObj(gearWhineOutputSample, "AudioDefaultLoop3D", "GearWhineOut", device.transmissionNodeID or sounds.engineNode)
  device.gearWhineInputLoop = sounds.createSoundObj(gearWhineInputSample, "AudioDefaultLoop3D", "GearWhineIn", device.transmissionNodeID or sounds.engineNode)


  local p2Sample = jbeamData.p2SoundSample or "event:>Vehicle>Transmission>electric_01>electric_out"
  device.p2SoundLoop = sounds.createSoundObj(p2Sample, "AudioDefaultLoop3D", "P2Motor", device.transmissionNodeID or sounds.exhuastNode)


  device.forwardInputPitchCoef = jbeamData.forwardInputPitchCoef or 1
  device.forwardOutputPitchCoef = jbeamData.forwardOutputPitchCoef or 1
  device.reverseInputPitchCoef = jbeamData.reverseInputPitchCoef or 0.7
  device.reverseOutputPitchCoef = jbeamData.reverseOutputPitchCoef or 0.7


  local inputAVSmoothing = jbeamData.gearWhineInputPitchCoefSmoothing or 50
  local outputAVSmoothing = jbeamData.gearWhineOutputPitchCoefSmoothing or 50
  local inputTorqueSmoothing = jbeamData.gearWhineInputVolumeCoefSmoothing or 10
  local outputTorqueSmoothing = jbeamData.gearWhineOutputVolumeCoefSmoothing or 10


  device.gearWhineInputTorqueSmoother = newExponentialSmoothing(inputTorqueSmoothing)
  device.gearWhineOutputTorqueSmoother = newExponentialSmoothing(outputTorqueSmoothing)
  device.gearWhineInputAVSmoother = newExponentialSmoothing(inputAVSmoothing)
  device.gearWhineOutputAVSmoother = newExponentialSmoothing(outputAVSmoothing)


  device.gearWhineInputAV = 0
  device.gearWhineOutputAV = 0
  device.gearWhineInputTorque = 0
  device.gearWhineOutputTorque = 0
end


local function reset(device, jbeamData)
  device.gearRatio = 0
  device.friction = jbeamData.friction or 0
  device.cumulativeGearRatio = 1
  device.maxCumulativeGearRatio = 1


  device.outputAV1 = 0
  device.inputAV = 0
  device.outputTorque1 = 0
  device.isBroken = false


  device.lockCoef = 1


  device.gearRatio1 = 0
  device.gearRatio2 = 0


  device.clutchAngle1 = 0
  device.clutchAngle2 = 0
  device.clutchRatio1 = 1
  device.clutchRatio2 = 1
  device.clutchRatio = 1
  device.torqueDiff1 = 0
  device.torqueDiff2 = 0
  device.torqueDiff = 0


  device.parkClutchAngle = 0


  device.damageLockTorqueCoef = 1
  device.wearLockTorqueCoef = 1


  device:setGearIndex1(1)
  device:setGearIndex2(2)


  selectUpdates(device)
end


local function new(jbeamData)
  print("[P2-GBX] NEW: Initializing p2DCTGearbox device...")
  local device = {
    deviceCategories = shallowcopy(M.deviceCategories),
    requiredExternalInertiaOutputs = shallowcopy(M.requiredExternalInertiaOutputs),
    outputPorts = shallowcopy(M.outputPorts),
    name = jbeamData.name,
    type = jbeamData.type,
    inputName = jbeamData.inputName,
    inputIndex = jbeamData.inputIndex,
    gearRatio = 0,
    friction = jbeamData.friction or 0,
    dynamicFriction = jbeamData.dynamicFriction or 0,
    torqueLossCoef = jbeamData.torqueLossCoef or 0,
    cumulativeGearRatio = 1,
    maxCumulativeGearRatio = 1,
    isPhysicallyDisconnected = true,
    outputAV1 = 0,
    inputAV = 0,
    outputTorque1 = 0,
    isBroken = false,
    lockCoef = 1,
    electricsClutchRatio1Name = jbeamData.electricsClutchRatio1Name or "clutchRatio1",
    electricsClutchRatio2Name = jbeamData.electricsClutchRatio2Name or "clutchRatio2",
    gearRatios = {},
    gearRatio1 = 0,
    gearRatio2 = 0,
    clutchAngle1 = 0,
    clutchAngle2 = 0,
    clutchRatio1 = 1,
    clutchRatio2 = 1,
    clutchRatio = 1,
    torqueDiff1 = 0,
    torqueDiff2 = 0,
    torqueDiff = 0,
    damageLockTorqueCoef = 1,
    wearLockTorqueCoef = 1,
    additionalEngineInertia = jbeamData.additionalEngineInertia or 0,
    reset = reset,
    initSounds = initSounds,
    resetSounds = resetSounds,
    updateSounds = updateSounds,
    setMode = setMode,
    validate = validate,
    setLock = setLock,
    calculateInertia = calculateInertia,
    setGearIndex1 = setGearIndex1,
    setGearIndex2 = setGearIndex2,
    applyDeformGroupDamage = applyDeformGroupDamage,
    setPartCondition = setPartCondition,
    getPartCondition = getPartCondition
  }


  device.shiftLogicName = "p2DCTGearbox"
  device.p2MaxTorque = jbeamData.p2MaxTorque or 280
  device.p2MaxRPM = jbeamData.p2MaxRPM or 8000
  device.p2MaxPower = (jbeamData.p2MaxPower * 1000) or 100000
  device.k0ClutchRatio = 1
  device.p2ShaftAV = 0
  device.p2MotorTorque = 0


  device.p2Volume = jbeamData.p2Volume / 100
  device.gearVolume = jbeamData.gearVolume / 100


  device.p2BatteryStorageName = jbeamData.p2BatteryStorageName or "mainBattery"
  device.p2MaxRegenTorque = jbeamData.p2MaxRegenTorque or 140
  device.p2RegenEfficiency = jbeamData.p2RegenEfficiency or 0.85
  device.p2DriveEfficiency = jbeamData.p2DriveEfficiency or 0.90
  device.isRegenActive = false


  device.torqueLossCoef = clamp(device.torqueLossCoef, 0, 1)
  device.clutchStiffness = jbeamData.clutchStiffness or 1
  device.lockDampRatio1 = jbeamData.lockDampRatio1 or 0.15
  device.lockDampRatio2 = jbeamData.lockDampRatio2 or 0.15
  device.lockTorque = jbeamData.lockTorque
  device.lockSpringBase = jbeamData.lockSpring


  device.parkClutchAngle = 0
  device.parkLockTorque = jbeamData.parkLockTorque or 1000
  device.parkLockDampRatio = jbeamData.parkLockDampRatio or 0.4
  device.parkLockSpringBase = jbeamData.parkLockSpring


  local forwardGears = {}
  local reverseGears = {}
  for _, v in pairs(jbeamData.gearRatios) do
    table.insert(v >= 0 and forwardGears or reverseGears, v)
  end


  device.maxGearIndex = 0
  device.minGearIndex = 0
  device.maxGearRatio = 0
  device.minGearRatio = 999999
  for i = 0, tableSize(forwardGears) - 1, 1 do
    device.gearRatios[i] = forwardGears[i + 1]
    device.maxGearIndex = max(device.maxGearIndex, i)
    device.maxGearRatio = max(device.maxGearRatio, abs(device.gearRatios[i]))
    if device.gearRatios[i] ~= 0 then
      device.minGearRatio = min(device.minGearRatio, abs(device.gearRatios[i]))
    end
  end
  local reverseGearCount = tableSize(reverseGears)
  for i = -reverseGearCount, -1, 1 do
    local index = -reverseGearCount - i - 1
    device.gearRatios[i] = reverseGears[abs(index)]
    device.minGearIndex = min(device.minGearIndex, index)
    device.maxGearRatio = max(device.maxGearRatio, abs(device.gearRatios[i]))
    if device.gearRatios[i] ~= 0 then
      device.minGearRatio = min(device.minGearRatio, abs(device.gearRatios[i]))
    end
  end
  device.gearCount = abs(device.maxGearIndex) + abs(device.minGearIndex)


  device.gearWhineCoefsOutput = {}
  for i = device.minGearIndex, device.maxGearIndex, 1 do
    device.gearWhineCoefsOutput[i] = 1.0
  end


  device.gearWhineCoefsInput = {}
  for i = device.minGearIndex, device.maxGearIndex, 1 do
    device.gearWhineCoefsInput[i] = 1.0
  end


  if jbeamData.gearboxNode_nodes and type(jbeamData.gearboxNode_nodes) == "table" then
    device.transmissionNodeID = jbeamData.gearboxNode_nodes[1]
  end


  device:setGearIndex1(1)
  device:setGearIndex2(2)
  selectUpdates(device)  local targetBattery = energyStorage.getStorage(device.p2BatteryStorageName)
  if not targetBattery then
    for name, storage in pairs(energyStorage.getStorages() or {}) do
      if storage.type == "electricBattery" or storage.energyType == "electricEnergy" then
        device.p2BatteryStorageName = name
        targetBattery = storage
        print(string.format("[P2-GBX] Linked P2 Motor to car's built-in battery: '%s' (%.2f kWh)", name, (storage.energyCapacity or 0) / 3600000))
        break
      end
    end
  else
    print(string.format("[P2-GBX] Linked P2 Motor to battery: '%s' (%.2f kWh)", device.p2BatteryStorageName, (targetBattery.energyCapacity or 0) / 3600000))
  end


  return device
end


M.new = new


return M


camso_advawd.lua

local M = {}
M.type = "auxiliary"
M.defaultOrder = 85


M.isActive = false
M.isActing = false


local Modes = {
    Normal = 0,
    TwoWD = 1,
    Locked = 2,
    Unknown = 3
}


local _currentMode = Modes.Normal
local _CMU = nil


local _controlParameters = {isEnabled = true}
local _initialControlParameters
local _relevantDifferential = nil


local _inputAVSmoother = newExponentialSmoothing(50)
local _outputAV1Smoother = newExponentialSmoothing(50)
local _outputAV2Smoother = newExponentialSmoothing(50)


local _outputTorque1 = 0
local _outputTorque2 = 0
local _inputAV = 0
local _outputAV1Corrected = 0
local _outputAV2Corrected = 0
local _totalAV = 0
local _totalTorque = 0
local _avDiff = 0


local _baseBias = 0.5
local _torqueSplit = 0
local _avSplit = 0
local _torqueOffset = 0
local _avOffset = 0
local _biasA = 0.5
local _biasB = 0.5
local _targetBias = 0.5
local _steeringCoef = 0


local function clamp(minVal, maxVal, value)
    return math.min(maxVal, math.max(minVal, value))
end


-- Safely switch differential mode in BeamNG
local function applyDiffMode(targetMode)
    if not _relevantDifferential then return end
    if _relevantDifferential.setMode then
        _relevantDifferential:setMode(targetMode)
    else
        _relevantDifferential.mode = targetMode
    end
end


local function processNormalMode(dt)
    _inputAV = _inputAVSmoother:get(_relevantDifferential.inputAV * _relevantDifferential.invGearRatio)
    _outputAV1Corrected = _outputAV1Smoother:get(_relevantDifferential.outputAV1)
    _outputAV2Corrected = _outputAV2Smoother:get(_relevantDifferential.outputAV2)
   
    _outputTorque1 = _relevantDifferential.outputTorque1
    _outputTorque2 = _relevantDifferential.outputTorque2
   
    if _outputTorque1 * sign(_inputAV) < 0 then
        _outputTorque1 = 0
        _outputAV1Corrected = 0
    end
    if _outputTorque2 * sign(_inputAV) < 0 then
        _outputTorque2 = 0
        _outputAV2Corrected = 0
    end
   
    _avDiff = math.abs((_outputAV2Corrected - _outputAV1Corrected) * sign(_inputAV))
    _totalTorque = math.abs(_outputTorque1) + math.abs(_outputTorque2)
    _totalAV = math.abs(_outputAV1Corrected) + math.abs(_outputAV2Corrected)
   
    _steeringCoef = electrics.values.wheelspeed > _controlParameters.minSteeringSpeed and (math.abs(electrics.values.steering or 0) / _controlParameters.steeringWheelLock) or 0
   
    if _totalAV > _controlParameters.avThreshold and _avDiff > _controlParameters.avDiffThreshold and _totalTorque > 0 then
        _torqueSplit = _outputTorque1 / _totalTorque * sign(_inputAV)
        _avSplit = math.abs(_outputAV1Corrected / _totalAV * sign(_inputAV))
       
        _torqueOffset = _torqueSplit - _baseBias
        _avOffset = (1 - _avSplit) - _baseBias
       
        _targetBias = (_torqueOffset + _avOffset) / 2
        if _steeringCoef > 0 then
            _targetBias = (_targetBias + _steeringCoef * _controlParameters.rearOutput) / 2
        end
       
        _targetBias = clamp(math.max(0.05, _baseBias - _controlParameters.maxBiasOffset),
                        math.min(0.95, _baseBias + _controlParameters.maxBiasOffset),
                        _targetBias + _baseBias)
    else
        _targetBias = _baseBias
    end
   
    if _targetBias < _biasA then
        _biasA = math.max(_targetBias, _biasA - _controlParameters.changeRate * dt)
    elseif _targetBias > _biasA then
        _biasA = math.min(_targetBias, _biasA + _controlParameters.changeRate * dt)
    end
               
    _biasB = 1 - _biasA
   
    _relevantDifferential.diffTorqueSplitA = _biasA
    _relevantDifferential.diffTorqueSplitB = _biasB
end


local function updateFixedStep(dt)
    if not _controlParameters.isEnabled or not _relevantDifferential then
        return
    end
   
    if _currentMode == Modes.Normal then
        processNormalMode(dt)
    else
        _relevantDifferential.diffTorqueSplitA = _biasA
        _relevantDifferential.diffTorqueSplitB = _biasB
    end
end


local function initSecondStage(jbeamData)
    if not _CMU then
        log("W", "camso_advawd.initSecondStage", "No _CMU present, disabling system...")
        shutdown()
        return
    end
   
    local diffName = jbeamData.differentialName
    if not diffName then
        log("E", "camso_advawd.initSecondStage", "No differentialName configured, disabling system...")
        return
    end
   
    _relevantDifferential = powertrain.getDevice(diffName)
   
    if not _relevantDifferential then
        log("E", "camso_advawd.initSecondStage", string.format("Can't find configured differential (%q), disabling system...", diffName))
        return
    end


    M.isActive = true
   
    -- 1. Read base torque split (e.g. 0.72 to the rear)
    _baseBias = _relevantDifferential.diffTorqueSplitA or 0.5
    _biasA = _baseBias
    _biasB = 1 - _baseBias
   
    -- 2. Store the JBeam's base mechanical LSD clutch parameters so we can restore them later
    _baseLsdLockCoef = _relevantDifferential.lsdLockCoef or 0.40
    _baseLsdRevLockCoef = _relevantDifferential.lsdRevLockCoef or 0.40
    _baseLsdPreload = _relevantDifferential.lsdPreload or 10.0
end


local function setMode(output)
    if not _relevantDifferential then return end


    if _currentMode == Modes.Normal then
        -- ========================================================
        -- RESTORE NORMAL AWD MODE (0.72 Rear / 0.28 Front + Active LSD)
        -- ========================================================
        -- Restore mechanical LSD clutches
        _relevantDifferential.lsdLockCoef = _baseLsdLockCoef
        _relevantDifferential.lsdRevLockCoef = _baseLsdRevLockCoef
        _relevantDifferential.lsdPreload = _baseLsdPreload


        -- Restore diff mode
        if _relevantDifferential.setMode then
            _relevantDifferential:setMode(_controlParameters.baseDiffMode or "lsd")
        else
            _relevantDifferential.mode = _controlParameters.baseDiffMode or "lsd"
        end


        -- Restore base torque split (e.g. 0.72 rear, 0.28 front)
        _targetBias = _baseBias
        _biasA = _baseBias
        _biasB = 1 - _baseBias
        _relevantDifferential.diffTorqueSplitA = _biasA
        _relevantDifferential.diffTorqueSplitB = _biasB


    elseif _currentMode == Modes.TwoWD then
        -- ========================================================
        -- TRUE 2WD DISCONNECT MODE (100% Rear, 0% Front, 0 Preload)
        -- ========================================================
        output = output or 1
        _biasA = (output == 1) and 1 or 0
        _biasB = 1 - _biasA
        _relevantDifferential.diffTorqueSplitA = _biasA
        _relevantDifferential.diffTorqueSplitB = _biasB


        -- Zero out the mechanical LSD clutch pack so the front driveshaft free-wheels completely
        _relevantDifferential.lsdLockCoef = 0
        _relevantDifferential.lsdRevLockCoef = 0
        _relevantDifferential.lsdPreload = 0
       
        if _relevantDifferential.setMode then
            _relevantDifferential:setMode("open")
        else
            _relevantDifferential.mode = "open"
        end


    elseif _currentMode == Modes.Locked then
        -- ========================================================
        -- 4WD LOCKED OFFROAD MODE
        -- ========================================================
        _relevantDifferential.lsdLockCoef = _baseLsdLockCoef
        _relevantDifferential.lsdRevLockCoef = _baseLsdRevLockCoef
        _relevantDifferential.lsdPreload = _baseLsdPreload


        _biasA = clamp(math.max(0.05, _baseBias - _controlParameters.maxBiasOffset),
                    math.min(0.95, _baseBias + _controlParameters.maxBiasOffset), 0.5)
        _biasB = 1 - _biasA
        _relevantDifferential.diffTorqueSplitA = _biasA
        _relevantDifferential.diffTorqueSplitB = _biasB


        if _relevantDifferential.setMode then
            _relevantDifferential:setMode("locked")
        else
            _relevantDifferential.mode = "locked"
        end
    end
end


local function setParameters(parameters)
    parameters = parameters or {}
   
    if parameters.mode then
        _currentMode = (parameters.mode == "normal" and Modes.Normal) or
                       (parameters.mode == "locked" and Modes.Locked) or
                       (parameters.mode == "2wd" and Modes.TwoWD) or
                       Modes.Unknown
    else
        _currentMode = Modes.Normal
    end
   
    -- Apply the mode and write changes directly to the differential
    setMode(parameters.output)
end


local function reset()
    M.isActing = false
    _targetBias = _baseBias
    _biasA = _baseBias
    _biasB = 1 - _baseBias
    _currentMode = Modes.Normal
    setMode(1)
end


local function init(jbeamData)
    M.isActing = false
   
    _controlParameters.isEnabled = true
    _controlParameters.avThreshold = jbeamData.avThreshold or 10
    _controlParameters.avDiffThreshold = jbeamData.avDiffThreshold or 1
    _controlParameters.maxBiasOffset = jbeamData.maxBiasOffset or 0.2
    _controlParameters.rearOutput = jbeamData.rearOutput and (jbeamData.rearOutput - 1) or 1
    _controlParameters.minSteeringSpeed = jbeamData.minSteeringSpeed or 8.33
    _controlParameters.baseDiffMode = jbeamData.baseDiffMode or "lsd"
    _controlParameters.mainOutput = clamp(0, 1, jbeamData.mainOutput or 1)
    _controlParameters.changeRate = jbeamData.changeRate or 1.5
    _controlParameters.steeringWheelLock = jbeamData.steeringWheelLock or 450
   
    _initialControlParameters = deepcopy(_controlParameters)
end


local function registerCMU(cmu)
    _CMU = cmu
end


local _baseLsdLockCoef = 0
local _baseLsdRevLockCoef = 0
local _baseLsdPreload = 0


local function setConfig(configTable)
    _controlParameters = configTable
end


local function getConfig()
    return deepcopy(_controlParameters)
end


local function shutdown()
    M.isActive = false
    M.isActing = false
    M.updateFixedStep = nil
end


M.init = init
M.initSecondStage = initSecondStage
M.reset = reset
M.updateFixedStep = updateFixedStep
M.registerCMU = registerCMU
M.shutdown = shutdown
M.setParameters = setParameters
M.setConfig = setConfig
M.getConfig = getConfig


return M


camso_electric_battery_abcde.jbeam

{
    "Camso_Battery_abcde": {
        "information":{
            "name":"Hybrid Battery",
            "value": 10000
        },
        "slotType" : "Camso_Hybrid_Battery",
        "energyStorage": [
            ["type", "name"],
            ["electricBattery", "mainBattery"]
        ],
        "variables": [
            ["name", "type", "unit", "category", "default", "min", "max", "title", "description"],
            ["$capacity", "range", "kWh", "Hybrid System", 8, 2, 15, "Battery Capacity", "Maximum battery charge", {"stepDis":0.1}],
            ["$charge", "range", "kWh", "Hybrid System", 8, 0, 15, "Battery Level", "Initial battery charge", {"stepDis":0.1}]
        ],
        "mainBattery": {
            "energyType":"electricEnergy",
            "batteryCapacity": "$capacity",
            "startingCapacity": "$charge"
        },

        "nodes": [
            ["id", "posX", "posY", "posZ"],
            {"nodeMaterial":"|NM_METAL|"},
            {"frictionCoef":0.5},
            {"collision":false},
            {"selfCollision":false},
            {"group":"hybrid_battery"},

            // Total weight = (capacity * 9kg) casing. 
            // We divide by 2 nodes placed low under the rear floor/chassis:
            {"nodeWeight": "$=($capacity * 9) / 2"},

            ["bat_L",  0.35, 0.85, 0.32],
            ["bat_R", -0.35, 0.85, 0.32]
        ],
        "beams": [
            ["id1:", "id2:"],
            {"beamPrecompression":1, "beamType":"|NORMAL", "beamLongBound":1, "beamShortBound":1},
            {"beamSpring": 2501000, "beamDamp": 150},
            {"beamDeform": 45000, "beamStrength": "FLT_MAX"},

            // Mount the battery rigidly to the vehicle's engine/chassis root node
            ["bat_L", "bat_R"],
            ["bat_L", "engine0"],
            ["bat_R", "engine0"]
        ]
    }
}

camso_transmission_abcde.jbeam

{
    "Camso_Transmission_abcde": {
        "information":{
            "authors":"Camshaft Software",
            "name":"8-Speed P2 Hybrid Transmission",
            "value": 4500
        },
        "slotType" : "Camso_Transmission",
        "slots": [
            ["type", "default", "description"],
            ["Camso_TransferCase", "Camso_TransferCase_AWD_abcde", "Drive Type"]
        ],
        
        "variables": [
            ["name", "type", "unit", "category", "default", "min", "max", "title", "description"],

            ["$gear_R_abcde", "range", ":1", "Gearing", 3.19, 0.1, 17, "Gear R", "Gear ratio", {"subCategory": "Transmission", "stepDis": 0.01}],
            ["$gear_1_abcde", "range", ":1", "Gearing", 3.04, 0.1, 17, "Gear 1", "Gear ratio", {"subCategory": "Transmission", "stepDis": 0.01}],
            ["$gear_2_abcde", "range", ":1", "Gearing", 2.36, 0.1, 17, "Gear 2", "Gear ratio", {"subCategory": "Transmission", "stepDis": 0.01}],
            ["$gear_3_abcde", "range", ":1", "Gearing", 1.73, 0.1, 17, "Gear 3", "Gear ratio", {"subCategory": "Transmission", "stepDis": 0.01}],
            ["$gear_4_abcde", "range", ":1", "Gearing", 1.30, 0.1, 17, "Gear 4", "Gear ratio", {"subCategory": "Transmission", "stepDis": 0.01}],
            ["$gear_5_abcde", "range", ":1", "Gearing", 1.02, 0.1, 17, "Gear 5", "Gear ratio", {"subCategory": "Transmission", "stepDis": 0.01}],
            ["$gear_6_abcde", "range", ":1", "Gearing", 0.82, 0.1, 17, "Gear 6", "Gear ratio", {"subCategory": "Transmission", "stepDis": 0.01}],
            ["$gear_7_abcde", "range", ":1", "Gearing", 0.68, 0.1, 17, "Gear 7", "Gear ratio", {"subCategory": "Transmission", "stepDis": 0.01}],
            ["$gear_8_abcde", "range", ":1", "Gearing", 0.57, 0.1, 17, "Gear 8", "Gear ratio", {"subCategory": "Transmission", "stepDis": 0.01}],
            ["$p2MotorTorque", "range", "Nm", "Hybrid System", 300, 50, 2000, "P2 Motor Peak Torque", "Input shaft electric torque", {"subCategory": "Electric Motor", "stepDis": 10}],
            ["$p2MotorPower", "range", "kW", "Hybrid System", 170, 30, 2000, "P2 Motor Max Power", "Input shaft electric power", {"subCategory": "Electric Motor", "stepDis": 10}],
            ["$p2MaxRPM", "range", "RPM", "Hybrid System", 7400, 1000, 20000, "P2 Motor Max RPM", "Motor max RPM", {"subCategory": "Electric Motor", "stepDis": 100}],
            ["$k0LockTorque", "range", "Nm", "Hybrid System", 1500, 0, 3000, "K0 Locking Force", "Clutch max Force", {"subCategory": "Clutch", "stepDis": 50}],
            ["$p2Volume", "range", "%", "Hybrid System", 100, 0, 300, "P2 Motor Volume", "Sound of the p2 motor", {"subCategory": "Sound", "stepDis": 1}],
            ["$gearVolume", "range", "%", "Hybrid System", 100, 0, 300, "Gear Whine Volume", "Sound of the gears whining", {"subCategory": "Sound", "stepDis": 1}],
        ],

        "controller": [
            ["fileName"],
        ],

        "powertrain" : [
            ["type", "name", "inputName", "inputIndex"],
            ["p2DCTGearbox", "gearbox", "mainEngine", 1]
        ],

        "gearbox": {
            "p2BatteryStorageName": "mainBattery", 
            "gearRatios": ["$=-$gear_R_abcde", 0, "$gear_1_abcde", "$gear_2_abcde", "$gear_3_abcde", "$gear_4_abcde", "$gear_5_abcde", "$gear_6_abcde", "$gear_7_abcde", "$gear_8_abcde"],
            "friction": 33.366359095467,
            "parkLockTorque": 1000,
            "p2MaxTorque": "$p2MotorTorque",       // Peak electric motor torque in Nm
            "p2MaxPower": "$p2MotorPower",     // 170 kW
            "p2MaxRPM": "$p2MaxRPM",         // Electric motor RPM ceiling
            "k0LockTorque": "$k0LockTorque"      // Engine disconnect clutch capacity (Nm)

            // --- SOUND CONFIGURATION (Reads directly into jbeamData) ---
            "gearboxNode:": ["engine0"],
            "p2SoundSample": "event:>Vehicle>Transmission>electric_02>electric_out"
            "gearWhineOutputEvent": "event:>Vehicle>Transmission>helical_01>twine_out",
            "gearWhineInputEvent": "event:>Vehicle>Transmission>helical_01>twine_in",
            "gearWhineCoefsInput": [1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
            "gearWhineCoefsOutput": [1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
            "p2Volume": "$p2Volume",
            "gearVolume": "$gearVolume",
            "forwardInputPitchCoef": 0.75,
            "forwardOutputPitchCoef": 0.75
        },

        "vehicleController": {
            "shiftLogicName": "p2DCTGearbox",
            "shiftDownRPMOffsetCoef": 1.0,
            "automaticModes": "PRNDSM",
            "clutchLaunchStartRPM": 1500,
            "clutchLaunchTargetRPM": 2000,
            "revMatchThrottle": 1.0,
            "transmissionShiftDelay": 0.0023744999215083,
            "dctClutchTime": 0.0079149997383611,
            "gearboxNode:": ["engine0"],
            "calculateOptimalLoadShiftPoints": true
        }
    }
}

camso_dse_drivemodes_abcde.jbeam

{
    "Camso_DriveModes_default_comfort_abcde": {
        "driveModes": {
            "defaultMode": "comfort"
        },
        "information": {
            "value": 500,
            "name": "EV Comfort Mode",
            "authors": "Camshaft Software"
        },
        "slotType": "Camso_DriveModes_default_abcde"
    },
    "Camso_DriveModes_default_ev2wd_abcde": {
        "driveModes": {
            "defaultMode": "ev2wd"
        },
        "information": {
            "value": 500,
            "name": "2WD EV Mode",
            "authors": "Camshaft Software"
        },
        "slotType": "Camso_DriveModes_default_abcde"
    },
    "Camso_DriveModes_default_sport_abcde": {
        "driveModes": {
            "defaultMode": "sport"
        },
        "information": {
            "value": 500,
            "name": "Sport Boost Mode",
            "authors": "Camshaft Software"
        },
        "slotType": "Camso_DriveModes_default_abcde"
    },
    "Camso_DriveModes_default_sport_ice_abcde": {
        "driveModes": {
            "defaultMode": "sport_ice"
        },
        "information": {
            "value": 500,
            "name": "Sport Pure ICE Mode",
            "authors": "Camshaft Software"
        },
        "slotType": "Camso_DriveModes_default_abcde"
    },
    "Camso_DriveModes_default_offroad_abcde": {
        "driveModes": {
            "defaultMode": "offroad"
        },
        "information": {
            "value": 500,
            "name": "Off-Road Mode",
            "authors": "Camshaft Software"
        },
        "slotType": "Camso_DriveModes_default_abcde"
    },
    "Camso_DriveModes_default_off2wd_abcde": {
        "driveModes": {
            "defaultMode": "off2wd"
        },
        "information": {
            "value": 500,
            "name": "2WD ESC & TC Off Mode",
            "authors": "Camshaft Software"
        },
        "slotType": "Camso_DriveModes_default_abcde"
    },
    "Camso_DriveModes_default_off_abcde": {
        "driveModes": {
            "defaultMode": "off"
        },
        "information": {
            "value": 500,
            "name": "ESC & TC Off Mode",
            "authors": "Camshaft Software"
        },
        "slotType": "Camso_DriveModes_default_abcde"
    },
    "Camso_DriveModes_offroad_abcde": {
        "driveModes": {
            "enabledModes_offroad": [
                "offroad"
            ]
        },
        "information": {
            "value": 500,
            "name": "Off-Road Mode",
            "authors": "Camshaft Software"
        },
        "slotType": "Camso_DriveModes_offroad_abcde"
    },
    "Camso_DriveModes_off2wd_abcde": {
        "driveModes": {
            "enabledModes_off2wd": [
                "off2wd"
            ]
        },
        "information": {
            "value": 500,
            "name": "2WD ESC & TC Off Mode",
            "authors": "Camshaft Software"
        },
        "slotType": "Camso_DriveModes_off2wd_abcde"
    },
    "Camso_DriveModes_abcde": {
        "slots": [
            [
                "type",
                "default",
                "description"
            ],
            [
                "Camso_DriveModes_default_abcde",
                "Camso_DriveModes_default_comfort_abcde",
                "Default Mode",
                {
                    "coreSlot": true
                }
            ],
            [
                "Camso_DriveModes_offroad_abcde",
                "",
                "Offroad Mode"
            ],
            [
                "Camso_DriveModes_off2wd_abcde",
                "",
                "2WD ESC & TC Off Mode"
            ],
            [
                "Camso_DriveModes_mod",
                "",
                "Additional Mode"
            ]
        ],
        "information": {
            "value": 250,
            "name": "GTE4 (DCT) Drive Modes",
            "authors": "Camshaft Software"
        },
        "driveModes": {
            "modes": {
                "comfort": {
                    "order": 10,
                    "name": "EV Comfort",
                    "settings": [
                        ["type"],
                        ["electricsValue", {"electricsName": "hybridMode", "value": "ev"}],
                        ["controller", {"controllerName": "CMU", "uiDisplayData.simplePowertrainApp.activeColor": "00FF88"}],
                        ["controller", {"controllerName": "adaptiveFrontDamper", "damperMode": "soft"}],
                        ["controller", {"controllerName": "adaptiveRearDamper", "damperMode": "soft"}],
                        ["controller", {
                            "tractionControl.wheelGroupSettings.mainEngine.slipThreshold": 0.08,
                            "tractionControl.isEnabled": true,
                            "yawControl.isEnabled": true,
                            "controllerName": "motorTorqueControl"
                        }],
                        ["controller", {
                            "controllerName": "brakeControl",
                            "yawControl.isEnabled": true,
                            "tractionControl.isEnabled": true
                        }],
                        ["controller", {"controllerName": "advawd", "mode": "normal"}]
                    ]
                },
                "ev2wd": {
                    "order": 15,
                    "name": "EV Sport 2WD",
                    "settings": [
                        ["type"],
                        ["electricsValue", {"electricsName": "hybridMode", "value": "ev"}],
                        ["controller", {"controllerName": "CMU", "uiDisplayData.simplePowertrainApp.activeColor": "00E5FF"}],
                        ["controller", {"controllerName": "adaptiveFrontDamper", "damperMode": "hard"}],
                        ["controller", {"controllerName": "adaptiveRearDamper", "damperMode": "hard"}],
                        ["controller", {
                            "tractionControl.wheelGroupSettings.mainEngine.slipThreshold": 0.5,
                            "tractionControl.isEnabled": true,
                            "yawControl.isEnabled": true,
                            "controllerName": "motorTorqueControl"
                        }],
                        ["controller", {
                            "controllerName": "brakeControl",
                            "yawControl.isEnabled": true,
                            "tractionControl.isEnabled": true
                        }],
                        ["controller", {"output": 1, "controllerName": "advawd", "mode": "2wd"}]
                    ]
                },
                "sport": {
                    "order": 20,
                    "name": "Sport Hybrid Boost",
                    "settings": [
                        ["type"],
                        ["electricsValue", {"electricsName": "hybridMode", "value": "sport"}],
                        ["controller", {"controllerName": "CMU", "uiDisplayData.simplePowertrainApp.activeColor": "FF2200"}],
                        ["controller", {"controllerName": "adaptiveFrontDamper", "damperMode": "hard"}],
                        ["controller", {"controllerName": "adaptiveRearDamper", "damperMode": "hard"}],
                        ["controller", {
                            "tractionControl.wheelGroupSettings.mainEngine.slipThreshold": 0.25,
                            "tractionControl.isEnabled": true,
                            "yawControl.isEnabled": true,
                            "controllerName": "motorTorqueControl"
                        }],
                        ["controller", {
                            "controllerName": "brakeControl",
                            "yawControl.isEnabled": true,
                            "tractionControl.isEnabled": true
                        }],
                        ["transmission", {"defaultForwardMode": "S"}],
                        ["controller", {"controllerName": "advawd", "mode": "normal"}]
                    ]
                },
                "sport_ice": {
                    "order": 25,
                    "name": "Sport ICE",
                    "settings": [
                        ["type"],
                        ["electricsValue", {"electricsName": "hybridMode", "value": "ice"}],
                        ["controller", {"controllerName": "CMU", "uiDisplayData.simplePowertrainApp.activeColor": "FF6600"}],
                        ["controller", {"controllerName": "adaptiveFrontDamper", "damperMode": "hard"}],
                        ["controller", {"controllerName": "adaptiveRearDamper", "damperMode": "hard"}],
                        ["controller", {
                            "tractionControl.wheelGroupSettings.mainEngine.slipThreshold": 0.25,
                            "tractionControl.isEnabled": true,
                            "yawControl.isEnabled": true,
                            "controllerName": "motorTorqueControl"
                        }],
                        ["controller", {
                            "controllerName": "brakeControl",
                            "yawControl.isEnabled": true,
                            "tractionControl.isEnabled": true
                        }],
                        ["transmission", {"defaultForwardMode": "S"}],
                        ["controller", {"controllerName": "advawd", "mode": "normal"}]
                    ]
                },
                "offroad": {
                    "order": 30,
                    "name": "Off-Road",
                    "settings": [
                        ["type"],
                        ["controller", {
                            "controllerName": "CMU",
                            "uiDisplayData.simplePowertrainApp.activeColor": "F5A442"
                        }],
                        ["controller", {
                            "controllerName": "adaptiveFrontDamper",
                            "damperMode": "soft"
                        }],
                        ["controller", {
                            "controllerName": "adaptiveRearDamper",
                            "damperMode": "soft"
                        }],
                        ["controller", {
                            "tractionControl.wheelGroupSettings.mainEngine.slipThreshold": 0.7,
                            "tractionControl.isEnabled": true,
                            "tractionControl.wheelGroupSettings.mainEngine.minimumThrottleLimit": 0.022596083316074,
                            "yawControl.isEnabled": false,
                            "controllerName": "motorTorqueControl"
                        }],
                        ["controller", {
                            "yawControl.isEnabled": false,
                            "tractionControl.wheelGroupSettings.mainEngine.slipThreshold": 0.4,
                            "controllerName": "brakeControl",
                            "tractionControl.isEnabled": true
                        }],
                        ["controller", {
                            "controllerName": "advawd",
                            "mode": "locked"
                        }]
                    ]
                },
                "off": {
                    "order": 50,
                    "name": "Off",
                    "settings": [
                        ["type"],
                        ["electricsValue", {"electricsName": "hybridMode", "value": "off"}],
                        ["controller", {"controllerName": "CMU", "uiDisplayData.simplePowertrainApp.activeColor": "DCDCDC"}],
                        ["controller", {"controllerName": "adaptiveFrontDamper", "damperMode": "hard"}],
                        ["controller", {"controllerName": "adaptiveRearDamper", "damperMode": "hard"}],
                        ["controller", {"controllerName": "tractionControl", "isEnabled": false}],
                        ["controller", {"controllerName": "yawControl", "isEnabled": false}],
                        ["transmission", {"defaultForwardMode": "S"}],
                        ["controller", {"controllerName": "advawd", "mode": "normal"}]
                    ]
                },
                "off2wd": {
                    "order": 45,
                    "name": "2WD, ESC & TC Off",
                    "settings": [
                        ["type"],
                        ["electricsValue", {"electricsName": "hybridMode", "value": "off"}],
                        ["controller", {"controllerName": "CMU", "uiDisplayData.simplePowertrainApp.activeColor": "CC1111"}],
                        ["controller", {"controllerName": "adaptiveFrontDamper", "damperMode": "hard"}],
                        ["controller", {"controllerName": "adaptiveRearDamper", "damperMode": "hard"}],
                        ["controller", {"controllerName": "tractionControl", "isEnabled": false}],
                        ["controller", {"controllerName": "yawControl", "isEnabled": false}],
                        ["transmission", {"defaultForwardMode": "S"}],
                        ["controller", {"output": 1, "controllerName": "advawd", "mode": "2wd"}]
                    ]
                }
            },
            "enabledModes": [
                "comfort",
                "ev2wd",
                "sport",
                "sport_ice",
                "off"
            ],
            "defaultMode": "comfort",
            "defaultSettings": [
                ["type"],
                ["controller", {"controllerName": "tractionControl", "isEnabled": true}],
                ["controller", {"controllerName": "yawControl", "isEnabled": true}],
                ["controller", {"controllerName": "adaptiveBrakeLights", "isEnabled": true}],
                ["controller", {"controllerName": "motorTorqueControl", "isEnabled": true}],
                ["controller", {"controllerName": "brakeControl", "isEnabled": true}]
            ]
        },
        "controller": [
            ["fileName"],
            ["driveModes"]
        ],
        "slotType": "Camso_DriveModes_abcde"
    }
}

Hopefully this was useful, this is a dump of what is inside the doc. You can use ctrl + h in any text editor and replace the abcde with like whatever your trim name is and it should work (maybe). Hopefully this kicks off a wave of cool hybrid cars man. Peace!

2 Likes

I’m definitely going to try this out, thanks for putting in the work so non programmers like me can enjoy this.