-- 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 = {}

M.outputPorts = {[1] = true} --set dynamically
M.deviceCategories = {engine = true}

local delayLine = require("delayLine")

local max = math.max
local min = math.min
local abs = math.abs
local floor = math.floor
local random = math.random

local rpmToAV = 0.104719755
local avToRPM = 9.549296596425384
local torqueToPower = 0.0001404345295653085
local psToWatt = 735.499
local hydrolockThreshold = 0.9

local function getTorqueData(device)
  local curves = {}
  local curveCounter = 1
  local maxTorque = 0
  local maxTorqueRPM = 0
  local maxPower = 0
  local maxPowerRPM = 0
  local maxRPM = device.maxRPM

  local turboCoefs = nil
  local superchargerCoefs = nil
  local nitrousTorques = nil

  local torqueCurve = {}
  local powerCurve = {}

  for k, v in pairs(device.torqueCurve) do
    if type(k) == "number" and k < maxRPM then
      torqueCurve[k + 1] = v - device.friction * device.wearFrictionCoef * device.damageFrictionCoef - (device.dynamicFriction * device.wearDynamicFrictionCoef * device.damageDynamicFrictionCoef * k * rpmToAV)
      powerCurve[k + 1] = torqueCurve[k + 1] * k * torqueToPower
      if torqueCurve[k + 1] > maxTorque then
        maxTorque = torqueCurve[k + 1]
        maxTorqueRPM = k + 1
      end
      if powerCurve[k + 1] > maxPower then
        maxPower = powerCurve[k + 1]
        maxPowerRPM = k + 1
      end
    end
  end

  table.insert(curves, curveCounter, {torque = torqueCurve, power = powerCurve, name = "NA", priority = 10})

  if device.nitrousOxideInjection.isExisting then
    local torqueCurveNitrous = {}
    local powerCurveNitrous = {}
    nitrousTorques = device.nitrousOxideInjection.getAddedTorque()

    for k, v in pairs(device.torqueCurve) do
      if type(k) == "number" and k < maxRPM then
        torqueCurveNitrous[k + 1] = v + (nitrousTorques[k] or 0) - device.friction * device.wearFrictionCoef * device.damageFrictionCoef - (device.dynamicFriction * device.wearDynamicFrictionCoef * device.damageDynamicFrictionCoef * k * rpmToAV)
        powerCurveNitrous[k + 1] = torqueCurveNitrous[k + 1] * k * torqueToPower
        if torqueCurveNitrous[k + 1] > maxTorque then
          maxTorque = torqueCurveNitrous[k + 1]
          maxTorqueRPM = k + 1
        end
        if powerCurveNitrous[k + 1] > maxPower then
          maxPower = powerCurveNitrous[k + 1]
          maxPowerRPM = k + 1
        end
      end
    end

    curveCounter = curveCounter + 1
    table.insert(curves, curveCounter, {torque = torqueCurveNitrous, power = powerCurveNitrous, name = "N2O", priority = 20})
  end

  if device.turbocharger.isExisting then
    local torqueCurveTurbo = {}
    local powerCurveTurbo = {}
    turboCoefs = device.turbocharger.getTorqueCoefs()

    for k, v in pairs(device.torqueCurve) do
      if type(k) == "number" and k < maxRPM then
        torqueCurveTurbo[k + 1] = (v * (turboCoefs[k] or 0)) - device.friction * device.wearFrictionCoef * device.damageFrictionCoef - (device.dynamicFriction * device.wearDynamicFrictionCoef * device.damageDynamicFrictionCoef * k * rpmToAV)
        powerCurveTurbo[k + 1] = torqueCurveTurbo[k + 1] * k * torqueToPower
        if torqueCurveTurbo[k + 1] > maxTorque then
          maxTorque = torqueCurveTurbo[k + 1]
          maxTorqueRPM = k + 1
        end
        if powerCurveTurbo[k + 1] > maxPower then
          maxPower = powerCurveTurbo[k + 1]
          maxPowerRPM = k + 1
        end
      end
    end

    curveCounter = curveCounter + 1
    table.insert(curves, curveCounter, {torque = torqueCurveTurbo, power = powerCurveTurbo, name = "Turbo", priority = 30})
  end

  if device.supercharger.isExisting then
    local torqueCurveSupercharger = {}
    local powerCurveSupercharger = {}
    superchargerCoefs = device.supercharger.getTorqueCoefs()

    for k, v in pairs(device.torqueCurve) do
      if type(k) == "number" and k < maxRPM then
        torqueCurveSupercharger[k + 1] = (v * (superchargerCoefs[k] or 0)) - device.friction * device.wearFrictionCoef * device.damageFrictionCoef - (device.dynamicFriction * device.wearDynamicFrictionCoef * device.damageDynamicFrictionCoef * k * rpmToAV)
        powerCurveSupercharger[k + 1] = torqueCurveSupercharger[k + 1] * k * torqueToPower
        if torqueCurveSupercharger[k + 1] > maxTorque then
          maxTorque = torqueCurveSupercharger[k + 1]
          maxTorqueRPM = k + 1
        end
        if powerCurveSupercharger[k + 1] > maxPower then
          maxPower = powerCurveSupercharger[k + 1]
          maxPowerRPM = k + 1
        end
      end
    end

    curveCounter = curveCounter + 1
    table.insert(curves, curveCounter, {torque = torqueCurveSupercharger, power = powerCurveSupercharger, name = "SC", priority = 40})
  end

  if device.turbocharger.isExisting and device.supercharger.isExisting then
    local torqueCurveFinal = {}
    local powerCurveFinal = {}

    for k, v in pairs(device.torqueCurve) do
      if type(k) == "number" and k < maxRPM then
        torqueCurveFinal[k + 1] = (v * (turboCoefs[k] or 0) * (superchargerCoefs[k] or 0)) - device.friction * device.wearFrictionCoef * device.damageFrictionCoef - (device.dynamicFriction * device.wearDynamicFrictionCoef * device.damageDynamicFrictionCoef * k * rpmToAV)
        powerCurveFinal[k + 1] = torqueCurveFinal[k + 1] * k * torqueToPower
        if torqueCurveFinal[k + 1] > maxTorque then
          maxTorque = torqueCurveFinal[k + 1]
          maxTorqueRPM = k + 1
        end
        if powerCurveFinal[k + 1] > maxPower then
          maxPower = powerCurveFinal[k + 1]
          maxPowerRPM = k + 1
        end
      end
    end

    curveCounter = curveCounter + 1
    table.insert(curves, curveCounter, {torque = torqueCurveFinal, power = powerCurveFinal, name = "Turbo + SC", priority = 50})
  end

  if device.turbocharger.isExisting and device.nitrousOxideInjection.isExisting then
    local torqueCurveFinal = {}
    local powerCurveFinal = {}

    for k, v in pairs(device.torqueCurve) do
      if type(k) == "number" and k < maxRPM then
        torqueCurveFinal[k + 1] = (v * (turboCoefs[k] or 0) + (nitrousTorques[k] or 0)) - device.friction * device.wearFrictionCoef * device.damageFrictionCoef - (device.dynamicFriction * device.wearDynamicFrictionCoef * device.damageDynamicFrictionCoef * k * rpmToAV)
        powerCurveFinal[k + 1] = torqueCurveFinal[k + 1] * k * torqueToPower
        if torqueCurveFinal[k + 1] > maxTorque then
          maxTorque = torqueCurveFinal[k + 1]
          maxTorqueRPM = k + 1
        end
        if powerCurveFinal[k + 1] > maxPower then
          maxPower = powerCurveFinal[k + 1]
          maxPowerRPM = k + 1
        end
      end
    end

    curveCounter = curveCounter + 1
    table.insert(curves, curveCounter, {torque = torqueCurveFinal, power = powerCurveFinal, name = "Turbo + N2O", priority = 60})
  end

  if device.supercharger.isExisting and device.nitrousOxideInjection.isExisting then
    local torqueCurveFinal = {}
    local powerCurveFinal = {}

    for k, v in pairs(device.torqueCurve) do
      if type(k) == "number" and k < maxRPM then
        torqueCurveFinal[k + 1] = (v * (superchargerCoefs[k] or 0) + (nitrousTorques[k] or 0)) - device.friction * device.wearFrictionCoef * device.damageFrictionCoef - (device.dynamicFriction * device.wearDynamicFrictionCoef * device.damageDynamicFrictionCoef * k * rpmToAV)
        powerCurveFinal[k + 1] = torqueCurveFinal[k + 1] * k * torqueToPower
        if torqueCurveFinal[k + 1] > maxTorque then
          maxTorque = torqueCurveFinal[k + 1]
          maxTorqueRPM = k + 1
        end
        if powerCurveFinal[k + 1] > maxPower then
          maxPower = powerCurveFinal[k + 1]
          maxPowerRPM = k + 1
        end
      end
    end

    curveCounter = curveCounter + 1
    table.insert(curves, curveCounter, {torque = torqueCurveFinal, power = powerCurveFinal, name = "SC + N2O", priority = 70})
  end

  if device.turbocharger.isExisting and device.supercharger.isExisting and device.nitrousOxideInjection.isExisting then
    local torqueCurveFinal = {}
    local powerCurveFinal = {}

    for k, v in pairs(device.torqueCurve) do
      if type(k) == "number" and k < maxRPM then
        torqueCurveFinal[k + 1] = (v * (turboCoefs[k] or 0) * (superchargerCoefs[k] or 0) + (nitrousTorques[k] or 0)) - device.friction * device.wearFrictionCoef * device.damageFrictionCoef - (device.dynamicFriction * device.wearDynamicFrictionCoef * device.damageDynamicFrictionCoef * k * rpmToAV)
        powerCurveFinal[k + 1] = torqueCurveFinal[k + 1] * k * torqueToPower
        if torqueCurveFinal[k + 1] > maxTorque then
          maxTorque = torqueCurveFinal[k + 1]
          maxTorqueRPM = k + 1
        end
        if powerCurveFinal[k + 1] > maxPower then
          maxPower = powerCurveFinal[k + 1]
          maxPowerRPM = k + 1
        end
      end
    end

    curveCounter = curveCounter + 1
    table.insert(curves, curveCounter, {torque = torqueCurveFinal, power = powerCurveFinal, name = "Turbo + SC + N2O", priority = 80})
  end

  table.sort(
    curves,
    function(a, b)
      local ra, rb = a.priority, b.priority
      if ra == rb then
        return a.name < b.name
      else
        return ra > rb
      end
    end
  )

  local dashes = {nil, {10, 4}, {8, 3, 4, 3}, {6, 3, 2, 3}, {5, 3}}
  for k, v in ipairs(curves) do
    v.dash = dashes[k]
    v.width = 2
  end

  return {maxRPM = maxRPM, curves = curves, maxTorque = maxTorque, maxPower = maxPower, maxTorqueRPM = maxTorqueRPM, maxPowerRPM = maxPowerRPM, finalCurveName = 1, deviceName = device.name, vehicleID = obj:getId()}
end

local function sendTorqueData(device, data)
  if not data then
    data = device:getTorqueData()
  end
  guihooks.trigger("TorqueCurveChanged", data)
end

local function scaleFrictionInitial(device, friction)
  device.friction = device.initialFriction * friction
end

local function scaleFriction(device, friction)
  device.friction = device.friction * friction
end

local function scaleOutputTorque(device, state, maxReduction)
  --scale torque ouput to some minimum, but do not let that minimum increase the actual scale (otherwise a min of 0.2 could "revive" and engine that sits at 0 scale already)
  device.outputTorqueState = max(device.outputTorqueState * state, min(maxReduction or 0, device.outputTorqueState))
  damageTracker.setDamage("engine", "engineReducedTorque", device.outputTorqueState < 1)
end

local function disable(device)
  device.outputTorqueState = 0
  device.isDisabled = true
  device.starterDisabled = true
  if device.starterEngagedCoef > 0 then
    device.starterEngagedCoef = 0
    obj:stopSFX(device.engineMiscSounds.starterSoundEngine)
    if device.engineMiscSounds.starterSoundExhaust then
      obj:stopSFX(device.engineMiscSounds.starterSoundExhaust)
    end
  end

  damageTracker.setDamage("engine", "engineDisabled", true)
end

local function enable(device)
  device.outputTorqueState = 1
  device.isDisabled = false
  device.starterDisabled = false

  damageTracker.setDamage("engine", "engineDisabled", false)
end

local function lockUp(device)
  device.outputTorqueState = 0
  device.outputAVState = 0
  device.isDisabled = true
  device.isBroken = true
  device.starterDisabled = true
  if device.starterEngagedCoef > 0 then
    device.starterEngagedCoef = 0
    obj:stopSFX(device.engineMiscSounds.starterSoundEngine)
    if device.engineMiscSounds.starterSoundExhaust then
      obj:stopSFX(device.engineMiscSounds.starterSoundExhaust)
    end
  end
  damageTracker.setDamage("powertrain", device.name, true)
  damageTracker.setDamage("engine", "engineLockedUp", true)
end

local function updateSounds(device, dt)
  local rpm = device.soundRPMSmoother:get(abs(device.outputAV1 * avToRPM), dt)
  local maxCurrentTorque = (device.torqueCurve[floor(rpm)] or 1) * device.intakeAirDensityCoef
  local engineLoad = device.soundLoadSmoother:get(device.instantEngineLoad, dt)
  local baseLoad = 0.3 * min(device.idleTorque / maxCurrentTorque, 1)
  engineLoad = max(engineLoad - baseLoad, 0) / (1 - baseLoad)
  local volumeCoef = rpm > 0.1 and device.engineVolumeCoef or 0

  if device.engineSoundID then
    local scaledEngineLoad = engineLoad * (device.soundMaxLoadMix - device.soundMinLoadMix) + device.soundMinLoadMix
    local fundamentalFreq = sounds.hzToFMODHz(rpm * device.soundConfiguration.engine.params.fundamentalFrequencyRPMCoef)
    obj:setEngineSound(device.engineSoundID, rpm, scaledEngineLoad, fundamentalFreq, volumeCoef)
  end

  if device.engineSoundIDExhaust then
    local minLoad = device.soundMinLoadMixExhaust or device.soundMinLoadMix
    local scaledEngineLoadExhaust = engineLoad * ((device.soundMaxLoadMixExhaust or device.soundMaxLoadMix) - minLoad) + minLoad
    local fundamentalFreqExhaust = sounds.hzToFMODHz(rpm * device.soundConfiguration.exhaust.params.fundamentalFrequencyRPMCoef)
    obj:setEngineSound(device.engineSoundIDExhaust, rpm, scaledEngineLoadExhaust, fundamentalFreqExhaust, volumeCoef)
  end

  device.turbocharger.updateSounds()
  device.supercharger.updateSounds()
end

local function checkHydroLocking(device, dt)
  if device.floodLevel > hydrolockThreshold then
    return
  end

  -- engine starts flooding if ALL of the waterDamage nodes are underwater
  local isFlooding = device.canFlood
  for _, n in ipairs(device.waterDamageNodes) do
    isFlooding = isFlooding and obj:inWater(n)
    if not isFlooding then
      break
    end
  end

  damageTracker.setDamage("engine", "engineIsHydrolocking", isFlooding)

  -- calculate flooding speed (positive) or drying speed (negative, and arbitrarily slower than flooding after some testing)
  local wetspeed = 1
  local dryspeed = -0.5
  local floodSpeed = (isFlooding and wetspeed or dryspeed) * (abs(device.outputAV1) / device.maxAV) -- TODO use torque instead of RPM (when torque calculation becomes more realistic)

  -- actual check for engine dying. in the future we may want to implement stalling too
  device.floodLevel = min(1, max(0, device.floodLevel + dt * floodSpeed))
  if device.floodLevel > hydrolockThreshold then
    damageTracker.setDamage("engine", "engineHydrolocked", true)
    -- avoid piston movement, simulate broken connecting rods
    device:lockUp()
    guihooks.message("vehicle.drivetrain.engineHydrolocked", 4, "vehicle.damage.flood")
    return
  end

  -- we compute the flooding percentage in steps of 10%...
  local currPercent = floor(0.5 + device.floodLevel * 10) * 10
  -- ...and use that to check when to perform UI updates
  if currPercent ~= device.prevFloodPercent then
    if currPercent > device.prevFloodPercent then
      guihooks.message({txt = "vehicle.drivetrain.engineFlooding", context = {percent = currPercent}}, 4, "vehicle.damage.flood")
    else
      if currPercent < 10 then
        guihooks.message("vehicle.drivetrain.engineDried", 4, "vehicle.damage.flood")
      else
        guihooks.message({txt = "vehicle.drivetrain.engineDrying", context = {percent = currPercent}}, 4, "vehicle.damage.flood")
      end
    end
  end
  device.prevFloodPercent = currPercent
end

local function updateEnergyStorageRatios(device)
  for _, s in pairs(device.registeredEnergyStorages) do
    local storage = energyStorage.getStorage(s)
    if storage and storage.energyType == device.requiredEnergyType then
      if storage.storedEnergy > 0 then
        device.energyStorageRatios[storage.name] = 1 / device.storageWithEnergyCounter
      else
        device.energyStorageRatios[storage.name] = 0
      end
    end
  end
end

local function updateFuelUsage(device)
  if not device.energyStorage then
    return
  end

  local hasFuel = false
  local previousTankCount = device.storageWithEnergyCounter
  local remainingFuelRatio = 0
  for _, s in pairs(device.registeredEnergyStorages) do
    local storage = energyStorage.getStorage(s)
    if storage and storage.energyType == device.requiredEnergyType then
      local previous = device.previousEnergyLevels[storage.name]
      storage.storedEnergy = max(storage.storedEnergy - (device.spentEnergy * device.energyStorageRatios[storage.name]), 0)
      if previous > 0 and storage.storedEnergy <= 0 then
        device.storageWithEnergyCounter = device.storageWithEnergyCounter - 1
      elseif previous <= 0 and storage.storedEnergy > 0 then
        device.storageWithEnergyCounter = device.storageWithEnergyCounter + 1
      end
      device.previousEnergyLevels[storage.name] = storage.storedEnergy
      hasFuel = hasFuel or storage.storedEnergy > 0
      remainingFuelRatio = remainingFuelRatio + storage.remainingRatio
    end
  end

  if previousTankCount ~= device.storageWithEnergyCounter then
    device:updateEnergyStorageRatios()
  end

  if not hasFuel and device.hasFuel then
    device:disable()
  elseif hasFuel and not device.hasFuel then
    device:enable()
  end

  device.hasFuel = hasFuel
  device.remainingFuelRatio = remainingFuelRatio / device.storageWithEnergyCounter
end

local function updateGFX(device, dt)
  device:updateFuelUsage()

  device.outputRPM = device.outputAV1 * avToRPM

  device.starterThrottleKillTimer = max(device.starterThrottleKillTimer - dt, 0)
  if device.starterEngagedCoef > 0 then
    -- if device.starterBattery then
    --   local starterSpentEnergy = 1 / guardZero(abs(device.outputAV1)) * dt * device.starterTorque / 0.5 --0.5 efficiency
    --   device.starterBattery.storedEnergy = device.starterBattery.storedEnergy - starterSpentEnergy
    -- --print(starterSpentEnergy)
    -- --print(device.starterBattery.remainingRatio)
    -- end

    if device.starterThrottleKillCoef < 1 and device.starterThrottleKillTimer <= 0 then
      device.starterThrottleKillCoef = 1
    end
    if device.outputAV1 > device.starterMaxAV * 1.1 then
      device.starterThrottleKillTimer = 0
      device.starterEngagedCoef = 0
      device.starterThrottleKillCoef = 1
      device.starterDisabled = false
      obj:stopSFX(device.engineMiscSounds.starterSoundEngine)
      if device.engineMiscSounds.starterSoundExhaust then
        obj:stopSFX(device.engineMiscSounds.starterSoundExhaust)
      end
    end
  end

  device.slowIgnitionErrorTimer = device.slowIgnitionErrorTimer - dt
  if device.slowIgnitionErrorTimer <= 0 then
    device.slowIgnitionErrorTimer = math.random(device.slowIgnitionErrorInterval) * 0.1
    device.slowIgnitionErrorActive = math.random() < device.slowIgnitionErrorChance
  end

  device.slowIgnitionErrorCoef = 1
  if device.slowIgnitionErrorActive then
    device.slowIgnitionErrorCoef = device.slowIgnitionErrorSmoother:getUncapped(math.random(), dt)
  end

  local lowFuelIgnitionErrorChance = linearScale(device.remainingFuelRatio, 0.01, 0, 0, 0.4)
  local fastIgnitionErrorCoef = device.fastIgnitionErrorSmoother:getUncapped(math.random(), dt)
  device.fastIgnitionErrorCoef = fastIgnitionErrorCoef < (device.fastIgnitionErrorChance + lowFuelIgnitionErrorChance) and 0 or 1

  if device.shutOffSoundRequested and device.outputAV1 < device.idleAV * 0.95 and device.outputAV1 > device.idleAV * 0.5 then
    device.shutOffSoundRequested = false

    if device.engineMiscSounds.shutOffSoundEngine then
      obj:cutSFX(device.engineMiscSounds.shutOffSoundEngine)
      obj:playSFX(device.engineMiscSounds.shutOffSoundEngine)
    end

    if device.engineMiscSounds.shutOffSoundExhaust then
      obj:cutSFX(device.engineMiscSounds.shutOffSoundExhaust)
      obj:playSFX(device.engineMiscSounds.shutOffSoundExhaust)
    end
  end

  if device.outputAV1 < device.starterMaxAV * 0.8 and device.ignitionCoef > 0 then
    device.stallTimer = max(device.stallTimer - dt, 0)
    if device.stallTimer <= 0 and not device.isStalled then
      device.isStalled = true
    end
  else
    device.isStalled = false
    device.stallTimer = 1
  end

  device.revLimiterWasActiveTimer = min(device.revLimiterWasActiveTimer + dt, 1000)

  local rpmTooHigh = abs(device.outputAV1) > device.maxPhysicalAV
  damageTracker.setDamage("engine", "overRevDanger", rpmTooHigh)
  if rpmTooHigh then
    device.overRevDamage = min(max(device.overRevDamage + (abs(device.outputAV1) - device.maxPhysicalAV) * dt / device.maxOverRevDamage, 0), 1)
    local lockupChance = random(60, 100) * 0.01
    local valveHitChance = random(10, 60) * 0.01
    if lockupChance <= device.overRevDamage and not damageTracker.getDamage("engine", "catastrophicOverrevDamage") then
      device:lockUp()
      damageTracker.setDamage("engine", "catastrophicOverrevDamage", true)
      guihooks.message({txt = "vehicle.drivetrain.engineCatastrophicOverrevDamage", context = {}}, 4, "vehicle.damage.catastrophicOverrev")

      if #device.engineBlockNodes >= 2 then
        sounds.playSoundOnceFollowNode("event:>Vehicle>Failures>engine_explode", device.engineBlockNodes[1], 1)

        for i = 1, 50 do
          local rnd = random()
          obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], i * rnd, 43, 0, 1)
          obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], i * rnd, 39, 0, 1)
          obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], -i * rnd, 43, 0, 1)
          obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], -i * rnd, 39, 0, 1)
        end
      end
    end
    if valveHitChance <= device.overRevDamage then
      device:scaleOutputTorque(0.98, 0.2)
      damageTracker.setDamage("engine", "mildOverrevDamage", true)
      guihooks.message({txt = "vehicle.drivetrain.engineMildOverrevDamage", context = {}}, 4, "vehicle.damage.mildOverrev")
    end
  end

  if device.maxTorqueRating > 0 then
    damageTracker.setDamage("engine", "overTorqueDanger", device.combustionTorque > device.maxTorqueRating)
    if device.combustionTorque > device.maxTorqueRating then
      local torqueDifference = device.combustionTorque - device.maxTorqueRating
      device.overTorqueDamage = min(device.overTorqueDamage + torqueDifference * dt, device.maxOverTorqueDamage)
      if device.overTorqueDamage >= device.maxOverTorqueDamage and not damageTracker.getDamage("engine", "catastrophicOverTorqueDamage") then
        device:lockUp()
        damageTracker.setDamage("engine", "catastrophicOverTorqueDamage", true)
        guihooks.message({txt = "vehicle.drivetrain.engineCatastrophicOverTorqueDamage", context = {}}, 4, "vehicle.damage.catastrophicOverTorque")

        if #device.engineBlockNodes >= 2 then
          sounds.playSoundOnceFollowNode("event:>Vehicle>Failures>engine_explode", device.engineBlockNodes[1], 1)

          for i = 1, 3 do
            local rnd = random()
            obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], i * rnd * 3, 43, 0, 9)
            obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], i * rnd * 3, 39, 0, 9)
            obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], -i * rnd * 3, 43, 0, 9)
            obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], -i * rnd * 3, 39, 0, 9)

            obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], i * rnd * 3, 56, 0, 1)
            obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], i * rnd * 3, 57, 0, 1)
            obj:addParticleByNodesRelative(device.engineBlockNodes[2], device.engineBlockNodes[1], i * rnd * 3, 58, 0, 1)
          end
        end
      end
    end
  end

  --calculate the actual current idle torque to check for lockup conditions due to high friction
  local idleThrottle = device.maxIdleThrottle
  local idleTorque = (device.torqueCurve[floor(abs(device.idleAV) * avToRPM)] or 0) * device.intakeAirDensityCoef
  local idleThrottleMap = min(max(idleThrottle + idleThrottle * device.maxPowerThrottleMap / (idleTorque * device.forcedInductionCoef * abs(device.outputAV1) + 1e-30) * (1 - idleThrottle), 0), 1)
  idleTorque = ((idleTorque * device.forcedInductionCoef * idleThrottleMap) + device.nitrousOxideTorque)

  if device.frictionTorque > device.maxTorque or (device.outputAV1 < device.idleAV * 0.5 and device.frictionTorque > idleTorque * 0.95) then
    --if our friction is higher than the biggest torque we can output, the engine WILL lock up automatically
    --however, we need to communicate that with other subsystems to prevent issues, so in this case we ADDITIONALLY lock it up manually
    device:lockUp()
  end

  device.exhaustFlowDelay:push(device.engineLoad)

  --push our summed fuels into the delay lines (shift fuel does not have any delay and therefore does not need a line)
  if device.shiftAfterFireFuel <= 0 then
    if device.instantAfterFireFuel > 0 then
      device.instantAfterFireFuelDelay:push(device.instantAfterFireFuel / dt)
    end
    if device.sustainedAfterFireFuel > 0 then
      device.sustainedAfterFireFuelDelay:push(device.sustainedAfterFireFuel / dt)
    end
  end

  if device.sustainedAfterFireTimer > 0 then
    device.sustainedAfterFireTimer = device.sustainedAfterFireTimer - dt
  elseif device.instantEngineLoad > 0 then
    device.sustainedAfterFireTimer = device.sustainedAfterFireTime
  end

  device.nitrousOxideTorque = 0 -- reset N2O torque
  device.engineVolumeCoef = 1 -- reset volume coef
  device.invBurnEfficiencyCoef = 1 -- reset burn efficiency coef

  device.turbocharger.updateGFX(dt)
  device.supercharger.updateGFX(dt)
  device.nitrousOxideInjection.updateGFX(dt)

  device.thermals.updateGFX(dt)

  device.intakeAirDensityCoef = obj:getRelativeAirDensity()

  device:checkHydroLocking(dt)

  device.idleAVReadError = device.idleAVReadErrorSmoother:getUncapped(device.idleAVReadErrorRangeHalf - random(device.idleAVReadErrorRange), dt) * device.wearIdleAVReadErrorRangeCoef * device.damageIdleAVReadErrorRangeCoef
  device.idleAVStartOffset = device.idleAVStartOffsetSmoother:get(device.idleAV * device.idleStartCoef * device.starterEngagedCoef, dt)
  device.maxIdleAV = device.idleAV + device.idleAVReadErrorRangeHalf * device.wearIdleAVReadErrorRangeCoef * device.damageIdleAVReadErrorRangeCoef
  device.minIdleAV = device.idleAV - device.idleAVReadErrorRangeHalf * device.wearIdleAVReadErrorRangeCoef * device.damageIdleAVReadErrorRangeCoef

  device.spentEnergy = 0
  device.spentEnergyNitrousOxide = 0
  device.engineWorkPerUpdate = 0
  device.frictionLossPerUpdate = 0
  device.pumpingLossPerUpdate = 0

  device.instantAfterFireFuel = 0
  device.sustainedAfterFireFuel = 0
  device.shiftAfterFireFuel = 0
  device.continuousAfterFireFuel = 0
end

local function setTempRevLimiter(device, revLimiterAV, maxOvershootAV)
  device.tempRevLimiterAV = revLimiterAV
  device.tempRevLimiterMaxAVOvershoot = maxOvershootAV or device.tempRevLimiterAV * 0.01
  device.invTempRevLimiterRange = 1 / device.tempRevLimiterMaxAVOvershoot
  device.isTempRevLimiterActive = true
end

local function resetTempRevLimiter(device)
  device.tempRevLimiterAV = device.maxAV * 10
  device.tempRevLimiterMaxAVOvershoot = device.tempRevLimiterAV * 0.01
  device.invTempRevLimiterRange = 1 / device.tempRevLimiterMaxAVOvershoot
  device.isTempRevLimiterActive = false
  device:setExhaustGainMufflingOffsetRevLimiter(0, 0)
end

local function revLimiterDisabledMethod(device, engineAV, throttle, dt)
  return throttle
end

local function revLimiterSoftMethod(device, engineAV, throttle, dt)
  local limiterAV = min(device.maxAV, device.tempRevLimiterAV)
  local correctedThrottle = -throttle * min(max(engineAV - limiterAV, 0), device.revLimiterMaxAVOvershoot) * device.invRevLimiterRange + throttle

  if device.isTempRevLimiterActive and correctedThrottle < throttle then
    device:setExhaustGainMufflingOffsetRevLimiter(-0.1, 2)
  end
  return correctedThrottle
end

local function revLimiterTimeMethod(device, engineAV, throttle, dt)
  local limiterAV = min(device.maxAV, device.tempRevLimiterAV)
  if device.revLimiterActive then
    device.revLimiterActiveTimer = device.revLimiterActiveTimer - dt
    local revLimiterAVThreshold = min(limiterAV - device.revLimiterMaxAVDrop, limiterAV)
    --Deactivate the limiter once below the deactivation threshold
    device.revLimiterActive = device.revLimiterActiveTimer > 0 and engineAV > revLimiterAVThreshold
    device.revLimiterWasActiveTimer = 0
    return 0
  end

  if engineAV > limiterAV and not device.revLimiterActive then
    device.revLimiterActiveTimer = device.revLimiterCutTime
    device.revLimiterActive = true
    device.revLimiterWasActiveTimer = 0
    return 0
  end

  return throttle
end

local function revLimiterRPMDropMethod(device, engineAV, throttle, dt)
  local limiterAV = min(device.maxAV, device.tempRevLimiterAV)
  if device.revLimiterActive or engineAV > limiterAV then
    --Deactivate the limiter once below the deactivation threshold
    local revLimiterAVThreshold = min(limiterAV - device.revLimiterAVDrop, limiterAV)
    device.revLimiterActive = engineAV > revLimiterAVThreshold
    device.revLimiterWasActiveTimer = 0
    return 0
  end

  return throttle
end

local function updateFixedStep(device, dt)
  device.forcedInductionCoef = 1
  device.turbocharger.updateFixedStep(dt)
  device.supercharger.updateFixedStep(dt)
end

--velocity update is always nopped for engines

local function updateTorque(device, dt)
  local engineAV = device.outputAV1

  local throttle = (electrics.values[device.electricsThrottleName] or 0) * (electrics.values[device.electricsThrottleFactorName] or device.throttleFactor)
  device.requestedThrottle = throttle
  
  device.idleAVTimer = device.idleAVTimer + dt -- ZEIT EDIT
  if device.idleAVTimer > 1/device.idleRPMVarianceFrequency then -- ZEIT EDIT
    if device.idleAVVariance > -device.idleRPMVariance*rpmToAV then -- ZEIT EDIT
      device.idleAVVariance = -device.idleRPMVariance*rpmToAV -- ZEIT EDIT
    else -- ZEIT EDIT
      device.idleAVVariance = device.idleRPMVariance*rpmToAV -- ZEIT EDIT
    end -- ZEIT EDIT
      device.idleAVTimer = 0 -- ZEIT EDIT
  end -- ZEIT EDIT

  local idleAV = max(device.idleAV, device.idleAVOverwrite) + device.idleRPMSmoother:get(device.idleAVVariance) -- ZEIT EDIT
  local maxIdleThrottle = min(max(device.maxIdleThrottle, device.maxIdleThrottleOverwrite), 1)
  local idleAVError = max(idleAV - engineAV + device.idleAVReadError + device.idleAVStartOffset, 0)
  local idleThrottle = max(throttle, min(idleAVError * 0.01, maxIdleThrottle))
  throttle = min(max(idleThrottle * device.starterThrottleKillCoef * device.ignitionCoef, 0), 1)

  throttle = device:applyRevLimiter(engineAV, throttle, dt)

  --smooth our actual throttle value to simulate various effects in a real engine that do not allow immediate throttle changes
  throttle = device.throttleSmoother:getUncapped(throttle, dt)

  local finalFriction = device.friction * device.wearFrictionCoef * device.damageFrictionCoef
  local finalDynamicFriction = device.dynamicFriction * device.wearDynamicFrictionCoef * device.damageDynamicFrictionCoef

  local torque = (device.torqueCurve[floor(engineAV * avToRPM)] or 0) * device.intakeAirDensityCoef
  local maxCurrentTorque = torque - finalFriction - (finalDynamicFriction * engineAV)
  --blend pure throttle with the constant power map
  local throttleMap = min(max(throttle + throttle * device.maxPowerThrottleMap / (torque * device.forcedInductionCoef * engineAV + 1e-30) * (1 - throttle), 0), 1)

  local ignitionCut = device.ignitionCutTime > 0
  torque = ((torque * device.forcedInductionCoef * throttleMap) + device.nitrousOxideTorque) * device.outputTorqueState * (ignitionCut and 0 or 1) * device.slowIgnitionErrorCoef * device.fastIgnitionErrorCoef

  local lastInstantEngineLoad = device.instantEngineLoad
  local instantLoad = min(max(torque / ((maxCurrentTorque + 1e-30) * device.outputTorqueState * device.forcedInductionCoef), 0), 1)
  device.instantEngineLoad = instantLoad
  device.engineLoad = device.loadSmoother:getCapped(device.instantEngineLoad, dt)

  local absEngineAV = abs(engineAV)
  local dtT = dt * torque
  local dtTNitrousOxide = dt * device.nitrousOxideTorque

  local burnEnergy = dtT * (dtT * device.halfInvEngInertia + engineAV)
  local burnEnergyNitrousOxide = dtTNitrousOxide * (dtTNitrousOxide * device.halfInvEngInertia + engineAV)
  device.engineWorkPerUpdate = device.engineWorkPerUpdate + burnEnergy
  device.frictionLossPerUpdate = device.frictionLossPerUpdate + finalFriction * absEngineAV * dt
  device.pumpingLossPerUpdate = device.pumpingLossPerUpdate + finalDynamicFriction * engineAV * engineAV * dt
  local invBurnEfficiency = device.invBurnEfficiencyTable[floor(device.instantEngineLoad * 100)] * device.invBurnEfficiencyCoef
  device.spentEnergy = device.spentEnergy + burnEnergy * invBurnEfficiency
  device.spentEnergyNitrousOxide = device.spentEnergyNitrousOxide + burnEnergyNitrousOxide * invBurnEfficiency

  local frictionTorque = finalFriction + finalDynamicFriction * absEngineAV + device.engineBrakeTorque * (1 - instantLoad)
  --friction torque is limited for stability
  frictionTorque = min(frictionTorque, absEngineAV * device.inertia * 2000) * sign(engineAV)

  local starterTorque = device.starterEngagedCoef * device.starterTorque * min(max(1 - engineAV / device.starterMaxAV, -0.5), 1)

  --iterate over all connected clutches and sum their torqueDiff to know the final torque load on the engine
  local torqueDiffSum = 0
  for i = 1, device.numberOfOutputPorts do
    torqueDiffSum = torqueDiffSum + device.clutchChildren[i].torqueDiff
  end
  --calculate the AV based on all loads
  local outputAV = (engineAV + dt * (torque - torqueDiffSum - frictionTorque + starterTorque) * device.invEngInertia) * device.outputAVState
  --set all output torques and AVs to the newly calculated values
  for i = 1, device.numberOfOutputPorts do
    device[device.outputTorqueNames[i]] = torqueDiffSum
    device[device.outputAVNames[i]] = outputAV
  end
  device.throttle = throttle
  device.combustionTorque = torque - frictionTorque
  device.frictionTorque = frictionTorque

  local inertialTorque = (device.outputAV1 - device.lastOutputAV1) * device.inertia / dt
  ffi.C.bng_applyTorqueAxisCouple(ffiObjPtr, inertialTorque, device.torqueReactionNodes[1], device.torqueReactionNodes[2], device.torqueReactionNodes[3])
  device.lastOutputAV1 = device.outputAV1

  local dLoad = min((device.instantEngineLoad - lastInstantEngineLoad) / dt, 0)
  local instantAfterFire = engineAV > device.idleAV * 2 and max(device.instantAfterFireCoef * -dLoad * lastInstantEngineLoad * absEngineAV, 0) or 0
  local sustainedAfterFire = (device.instantEngineLoad <= 0 and device.sustainedAfterFireTimer > 0) and max(engineAV * device.sustainedAfterFireCoef, 0) or 0

  device.instantAfterFireFuel = device.instantAfterFireFuel + instantAfterFire
  device.sustainedAfterFireFuel = device.sustainedAfterFireFuel + sustainedAfterFire
  device.shiftAfterFireFuel = device.shiftAfterFireFuel + instantAfterFire * (ignitionCut and 1 or 0)

  device.lastOutputTorque = torque
  device.ignitionCutTime = max(device.ignitionCutTime - dt, 0)

  device.fixedStepTimer = device.fixedStepTimer + dt
  if device.fixedStepTimer >= device.fixedStepTime then
    device:updateFixedStep(device.fixedStepTimer)
    device.fixedStepTimer = device.fixedStepTimer - device.fixedStepTime
  end
end

local function selectUpdates(device)
  device.velocityUpdate = nop
  device.torqueUpdate = updateTorque
end

local function applyDeformGroupDamage(device, damageAmount, groupType)
  if groupType == "main" then
    device.damageFrictionCoef = device.damageFrictionCoef + linearScale(damageAmount, 0, 0.01, 0, 0.1)
    device.damageDynamicFrictionCoef = device.damageDynamicFrictionCoef + linearScale(damageAmount, 0, 0.01, 0, 0.1)
    device.damageIdleAVReadErrorRangeCoef = device.damageIdleAVReadErrorRangeCoef + linearScale(damageAmount, 0, 0.01, 0, 0.5)
    device.fastIgnitionErrorChance = min(device.fastIgnitionErrorChance + linearScale(damageAmount, 0, 0.01, 0, 0.05))
    device.slowIgnitionErrorChance = min(device.slowIgnitionErrorChance + linearScale(damageAmount, 0, 0.01, 0, 0.05))
    damageTracker.setDamage("engine", "impactDamage", true, true)
  elseif groupType == "radiator" and device.thermals.applyDeformGroupDamageRadiator then
    device.thermals.applyDeformGroupDamageRadiator(damageAmount)
  elseif groupType == "oilPan" and device.thermals.applyDeformGroupDamageOilpan then
    device.thermals.applyDeformGroupDamageOilpan(damageAmount)
  elseif groupType == "oilRadiator" and device.thermals.applyDeformGroupDamageOilRadiator then
    device.thermals.applyDeformGroupDamageOilRadiator(damageAmount)
  elseif groupType == "turbo" and device.turbocharger.applyDeformGroupDamage then
    device.turbocharger.applyDeformGroupDamage(damageAmount)
  elseif groupType == "supercharger" and device.supercharger.applyDeformGroupDamage then
    device.supercharger.applyDeformGroupDamage(damageAmount)
  end
end

local function setPartCondition(device, subSystem, odometer, integrity, visual)
  if not subSystem then
    device.wearFrictionCoef = linearScale(odometer, 30000000, 1000000000, 1, 1.0)
    device.wearDynamicFrictionCoef = linearScale(odometer, 30000000, 1000000000, 1, 1.5)
    device.wearIdleAVReadErrorRangeCoef = linearScale(odometer, 30000000, 500000000, 1, 10)
    local integrityState = integrity
    if type(integrity) == "number" then
      local integrityValue = integrity
      integrityState = {
        damageFrictionCoef = linearScale(integrityValue, 1, 0, 1, 1.0),
        damageDynamicFrictionCoef = linearScale(integrityValue, 1, 0, 1, 1.5),
        damageIdleAVReadErrorRangeCoef = linearScale(integrityValue, 1, 0, 1, 30),
        fastIgnitionErrorChance = linearScale(integrityValue, 1, 0, 0, 0.4),
        slowIgnitionErrorChance = linearScale(integrityValue, 1, 0, 0, 0.4)
      }
    end

    device.damageFrictionCoef = integrityState.damageFrictionCoef or 1
    device.damageDynamicFrictionCoef = integrityState.damageDynamicFrictionCoef or 1
    device.damageIdleAVReadErrorRangeCoef = integrityState.damageIdleAVReadErrorRangeCoef or 1
    device.fastIgnitionErrorChance = integrityState.fastIgnitionErrorChance
    device.slowIgnitionErrorChance = integrityState.slowIgnitionErrorChance

    if integrityState.isBroken then
      device:onBreak()
    end
  elseif subSystem == "radiator" then
    device.thermals.setPartConditionRadiator(odometer, integrity, visual)
  elseif subSystem == "exhaust" then
    device.thermals.setPartConditionExhaust(odometer, integrity, visual)
  elseif subSystem == "turbocharger" then
    device.turbocharger.setPartCondition(odometer, integrity, visual)
  -- elseif subSystem == "supercharger" then
  --   device.supercharger.setPartCondition(odometer, integrity, visual)
  end
end

local function getPartCondition(device, subSystem)
  if not subSystem then
    local integrityState = {
      damageFrictionCoef = device.damageFrictionCoef,
      damageDynamicFrictionCoef = device.damageDynamicFrictionCoef,
      damageIdleAVReadErrorRangeCoef = device.damageIdleAVReadErrorRangeCoef,
      fastIgnitionErrorChance = device.fastIgnitionErrorChance,
      slowIgnitionErrorChance = device.slowIgnitionErrorChance,
      isBroken = device.isBroken
    }

    local frictionIntegrityValue = linearScale(device.damageFrictionCoef, 1, 5, 1, 0)
    local dynamicFrictionIntegrityValue = linearScale(device.damageDynamicFrictionCoef, 1, 5, 1, 0)
    local idleAVReadErrorRangeIntegrityValue = linearScale(device.damageIdleAVReadErrorRangeCoef, 1, 50, 1, 0)
    local slowIgnitionErrorIntegrityValue = linearScale(device.slowIgnitionErrorChance, 0, 0.4, 1, 0)
    local fastIgnitionErrorIntegrityValue = linearScale(device.fastIgnitionErrorChance, 0, 0.4, 1, 0)

    local integrityValue = min(frictionIntegrityValue, dynamicFrictionIntegrityValue, idleAVReadErrorRangeIntegrityValue, slowIgnitionErrorIntegrityValue, fastIgnitionErrorIntegrityValue)
    if device.isBroken then
      integrityValue = 0
    end
    return integrityValue, integrityState
  elseif subSystem == "exhaust" then
    local integrityValue, integrityState = device.thermals.getPartConditionExhaust()
    return integrityValue, integrityState
  elseif subSystem == "radiator" then
    local integrityValue, integrityState = device.thermals.getPartConditionRadiator()
    return integrityValue, integrityState
  elseif subSystem == "turbocharger" then
    local integrityValue, integrityState = device.turbocharger.getPartCondition()
    return integrityValue, integrityState
  -- elseif subSystem == "supercharger" then
  --   local integrityValue, integrityState = device.supercharger.getPartCondition()
  --   return integrityValue, integrityState
  end
end

local function validate(device)
  if not device.children or #device.children < 1 then
    device.clutchChildren = {{torqueDiff = 0}}
  else
    device.clutchChildren = {}
    for _, child in ipairs(device.children) do
      if child.deviceCategories.clutchlike then
        table.insert(device.clutchChildren, child)
        device.inertia = device.inertia + (child.additionalEngineInertia or 0)
      else
        log("E", "combustionEngine.validate", "Found a non clutchlike device as child of a combustion engine!")
        log("E", "combustionEngine.validate", "Child data:")
        log("E", "combustionEngine.validate", powertrain.dumpsDeviceData(child))
        return false
      end
    end
    device.invEngInertia = 1 / device.inertia
    device.halfInvEngInertia = device.invEngInertia * 0.5
  end
  device.initialInertia = device.inertia

  table.insert(
    powertrain.engineData,
    {
      maxRPM = device.maxRPM,
      maxSoundRPM = device.hasRevLimiter and device.maxRPM or device.maxAvailableRPM,
      torqueReactionNodes = device.torqueReactionNodes
    }
  )

  return true
end

local function activateStarter(device)
  device.ignitionCoef = 1
  if device.starterEngagedCoef ~= 1 and not device.isDisabled then
    device.starterThrottleKillCoef = 0
    local coldBlockStartTimeCoef = device.requiredEnergyType == "diesel" and 4 or 2
    device.starterThrottleKillTimer = device.starterThrottleKillTime * linearScale(device.thermals.engineBlockTemperature, -20, 20, coldBlockStartTimeCoef, 1)
    device.starterEngagedCoef = 1

    obj:cutSFX(device.engineMiscSounds.starterSoundEngine)
    obj:playSFX(device.engineMiscSounds.starterSoundEngine)

    if device.engineMiscSounds.starterSoundExhaust then
      obj:cutSFX(device.engineMiscSounds.starterSoundExhaust)
      obj:playSFX(device.engineMiscSounds.starterSoundExhaust)
    end

    device.engineMiscSounds.loopTimer = device.engineMiscSounds.loopTime
  end
end

local function cutIgnition(device, time)
  device.ignitionCutTime = time
end

local function deactivateStarter(device)
  device.starterThrottleKillTimer = 0
  device.starterEngagedCoef = 0
  obj:stopSFX(device.engineMiscSounds.starterSoundEngine)
  if device.engineMiscSounds.starterSoundExhaust then
    obj:stopSFX(device.engineMiscSounds.starterSoundExhaust)
  end
end

local function setIgnition(device, value)
  device.ignitionCoef = value > 0 and 1 or 0
  if value == 0 then
    device.starterThrottleKillTimer = 0
    device.starterEngagedCoef = 0
    device.shutOffSoundRequested = true
  end
end

local function onBreak(device)
  device:lockUp()
end

local function beamBroke(device, id)
  device.thermals.beamBroke(id)
end

local function registerStorage(device, storageName)
  local storage = energyStorage.getStorage(storageName)
  if not storage then
    return
  end
  if storage.type == "n2oTank" then
    device.nitrousOxideInjection.registerStorage(storageName)
  elseif storage.type == "electricBattery" then
    device.starterBattery = storage
  elseif storage.energyType == device.requiredEnergyType then
    if storage.storedEnergy > 0 then
      device.storageWithEnergyCounter = device.storageWithEnergyCounter + 1
      table.insert(device.registeredEnergyStorages, storageName)
      device:updateEnergyStorageRatios()
    end
    device.hasFuel = true
    device.previousEnergyLevels[storageName] = storage.storedEnergy
  end
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

  device.cumulativeInertia = outputInertia
  device.cumulativeGearRatio = cumulativeGearRatio
  device.maxCumulativeGearRatio = maxCumulativeGearRatio
end

local function initEngineSound(device, soundID, samplePath, engineNodeIDs, offLoadGain, onLoadGain, reference)
  device.soundConfiguration[reference] = device.soundConfiguration[reference] or {}
  device.soundConfiguration[reference].blendFile = samplePath
  obj:queueGameEngineLua(string.format("core_sounds.initEngineSound(%d,%d,%q,%s,%f,%f)", objectId, soundID, samplePath, serialize(engineNodeIDs), offLoadGain, onLoadGain))
end

local function initExhaustSound(device, soundID, samplePath, engineNodeIDPairs, offLoadGain, onLoadGain, reference)
  device.soundConfiguration[reference] = device.soundConfiguration[reference] or {}
  device.soundConfiguration[reference].blendFile = samplePath
  obj:queueGameEngineLua(string.format("core_sounds.initExhaustSound(%d,%d,%q,%s,%f,%f)", objectId, soundID, samplePath, serialize(engineNodeIDPairs), offLoadGain, onLoadGain))
end

local function setExhaustSoundNodes(soundID, nodeIDPairs)
  obj:queueGameEngineLua(string.format("core_sounds.setExhaustSoundNodes(%d,%d,%s)", objectId, soundID, serialize(nodeIDPairs)))
end

local function setEngineSoundParameterList(device, soundID, params, reference)
  params.main_gain = params.base_gain + params.gainOffset + params.gainOffsetRevLimiter
  params.muffled = params.base_muffled + params.mufflingOffset + params.mufflingOffsetRevLimiter

  device.soundConfiguration[reference] = device.soundConfiguration[reference] or {}
  device.soundConfiguration[reference].params = tableMergeRecursive(device.soundConfiguration[reference].params or {}, params)
  device.soundConfiguration[reference].soundID = soundID
  obj:queueGameEngineLua(string.format("core_sounds.setEngineSoundParameterList(%d,%d,%s)", objectId, soundID, serialize(params)))

  --print(reference)
  --print(params.eq_e_gain)
  if reference == "engine" then
    if device.engineMiscSounds.starterSoundEngine then
      obj:setVolumePitchCT(device.engineMiscSounds.starterSoundEngine, device.engineMiscSounds.starterVolume, 1, params.main_gain, 0)
    end
    if device.engineMiscSounds.shutOffSoundEngine then
      obj:setVolumePitchCT(device.engineMiscSounds.shutOffSoundEngine, device.engineMiscSounds.shutOffVolumeEngine, 1, params.main_gain, 0)
    end
  elseif reference == "exhaust" then
    if device.engineMiscSounds.starterSoundExhaust then
      obj:setVolumePitchCT(device.engineMiscSounds.starterSoundExhaust, device.engineMiscSounds.starterVolumeExhaust, 1, params.main_gain, 0)
    end
    if device.engineMiscSounds.shutOffSoundExhaust then
      obj:setVolumePitchCT(device.engineMiscSounds.shutOffSoundExhaust, device.engineMiscSounds.shutOffVolumeExhaust, 1, params.main_gain, 0)
    end
  end
end

local function exhaustEndNodesChanged(device, endNodes)
  if device.engineSoundIDExhaust then
    local endNodeIDPairs
    local maxExhaustAudioOpennessCoef = 0
    local maxExhaustAudioGain
    if endNodes and #endNodes > 0 then
      endNodeIDPairs = {}
      for _, v in pairs(endNodes) do
        maxExhaustAudioOpennessCoef = min(max(maxExhaustAudioOpennessCoef, v.exhaustAudioOpennessCoef), 1)
        maxExhaustAudioGain = maxExhaustAudioGain and max(maxExhaustAudioGain, v.exhaustAudioGainChange) or v.exhaustAudioGainChange
        table.insert(endNodeIDPairs, {v.start, v.finish})
      end
    else
      endNodeIDPairs = {{device.engineNodeID, device.engineNodeID}}
      maxExhaustAudioGain = 0
    end
    setExhaustSoundNodes(device.engineSoundIDExhaust, endNodeIDPairs)

    local params = {
      base_muffled = device.exhaustAudioMufflingMinCoef + device.exhaustAudioMufflingCoefRange * (1 - maxExhaustAudioOpennessCoef),
      base_gain = device.exhaustMainGain + maxExhaustAudioGain,
      gainOffset = 0,
      mufflingOffset = 0,
      mufflingOffsetRevLimiter = 0,
      gainOffsetRevLimiter = 0
    }
    device:setEngineSoundParameterList(device.engineSoundIDExhaust, params, "exhaust")
  end
end

local function getSoundConfiguration(device)
  return device.soundConfiguration
end

local function setExhaustGainMufflingOffset(device, mufflingOffset, gainOffset)
  if not (device.soundConfiguration and device.soundConfiguration.exhaust) then
    return
  end

  local currentConfig = device.soundConfiguration.exhaust
  currentConfig.params.mufflingOffset = mufflingOffset
  currentConfig.params.gainOffset = gainOffset

  device:setEngineSoundParameterList(device.engineSoundIDExhaust, currentConfig.params, "exhaust")
end

local function setExhaustGainMufflingOffsetRevLimiter(device, mufflingOffset, gainOffset)
  if not (device.soundConfiguration and device.soundConfiguration.exhaust) then
    return
  end

  local currentConfig = device.soundConfiguration.exhaust
  currentConfig.params.mufflingOffsetRevLimiter = mufflingOffset
  currentConfig.params.gainOffsetRevLimiter = gainOffset

  device:setEngineSoundParameterList(device.engineSoundIDExhaust, currentConfig.params, "exhaust")
end

local function resetSounds(device, jbeamData)
  if not sounds.usesOldCustomSounds then
    if jbeamData.soundConfig then
      local soundConfig = v.data[jbeamData.soundConfig]
      if soundConfig then
        device.soundRPMSmoother:reset()
        device.soundLoadSmoother:reset()
        device.engineVolumeCoef = 1
        --dump(sounds)
        sounds.disableOldEngineSounds()
      else
        log("E", "combustionEngine.init", "Can't find sound config: " .. jbeamData.soundConfig)
      end
      if device.engineSoundIDExhaust then
        local endNodeIDPairs
        local maxExhaustAudioOpennessCoef = 0
        local maxExhaustAudioGain
        if device.thermals.exhaustEndNodes and #device.thermals.exhaustEndNodes > 0 then
          endNodeIDPairs = {}
          for _, v in pairs(device.thermals.exhaustEndNodes) do
            maxExhaustAudioOpennessCoef = min(max(maxExhaustAudioOpennessCoef, v.exhaustAudioOpennessCoef), 1)
            maxExhaustAudioGain = maxExhaustAudioGain and max(maxExhaustAudioGain, v.exhaustAudioGainChange) or v.exhaustAudioGainChange
            table.insert(endNodeIDPairs, {v.start, v.finish})
          end
        else
          endNodeIDPairs = {{device.engineNodeID, device.engineNodeID}}
          maxExhaustAudioGain = 0
        end
        setExhaustSoundNodes(device.engineSoundIDExhaust, endNodeIDPairs)
        local params = {
          base_muffled = device.exhaustAudioMufflingMinCoef + device.exhaustAudioMufflingCoefRange * (1 - maxExhaustAudioOpennessCoef),
          base_gain = device.exhaustMainGain + maxExhaustAudioGain,
          gainOffset = 0,
          mufflingOffset = 0,
          mufflingOffsetRevLimiter = 0,
          gainOffsetRevLimiter = 0
        }
        device:setEngineSoundParameterList(device.engineSoundIDExhaust, params, "exhaust")
      end
    end
  else
    log("W", "combustionEngine.init", "Disabling new sounds, found old custom engine sounds...")
  end

  device.turbocharger.resetSounds(v.data[jbeamData.turbocharger])
  device.supercharger.resetSounds(v.data[jbeamData.supercharger])
  device.nitrousOxideInjection.resetSounds(v.data[jbeamData.nitrousOxideInjection])
  device.thermals.resetSounds(jbeamData)
end

local function reset(device, jbeamData)
  local spawnWithEngineRunning = device.spawnVehicleIgnitionLevel > 2
  local spawnWithIgnitionOn = device.spawnVehicleIgnitionLevel > 1

  --reset output AVs and torques
  for i = 1, device.numberOfOutputPorts, 1 do
    device[device.outputTorqueNames[i]] = 0
    device[device.outputAVNames[i]] = spawnWithEngineRunning and (jbeamData.idleRPM * rpmToAV) or 0
  end
  device.outputRPM = device.outputAV1 * avToRPM
  device.lastOutputAV1 = device.outputAV1
  device.ignitionCoef = spawnWithIgnitionOn and 1 or 0

  device.friction = jbeamData.friction or 0
  device.inputAV = 0
  device.virtualMassAV = 0
  device.isBroken = false
  device.combustionTorque = 0
  device.frictionTorque = 0
  device.nitrousOxideTorque = 0

  device.electricsThrottleName = jbeamData.electricsThrottleName or "throttle"
  device.electricsThrottleFactorName = jbeamData.electricsThrottleFactorName or "throttleFactor"
  device.throttleFactor = 1

  device.throttle = 0
  device.requestedThrottle = 0
  device.dynamicFriction = jbeamData.dynamicFriction or 0

  device.idleAVReadError = 0
  device.idleAVStartOffset = 0
  device.inertia = device.initialInertia
  device.invEngInertia = 1 / device.inertia
  device.halfInvEngInertia = device.invEngInertia * 0.5

  device.slowIgnitionErrorSmoother:reset()
  device.slowIgnitionErrorTimer = 0
  device.slowIgnitionErrorChance = 0.0
  device.slowIgnitionErrorCoef = 1
  device.fastIgnitionErrorSmoother:reset()
  device.fastIgnitionErrorChance = 0.0
  device.fastIgnitionErrorCoef = 1

  device.starterEngagedCoef = 0
  device.starterThrottleKillCoef = 1
  device.starterThrottleKillTimer = 0
  device.starterDisabled = false
  device.idleAVStartOffsetSmoother:reset()
  device.shutOffSoundRequested = false

  device.stallTimer = 1
  device.isStalled = false

  device.floodLevel = 0
  device.prevFloodPercent = 0

  device.forcedInductionCoef = 1
  device.intakeAirDensityCoef = 1
  device.outputTorqueState = 1
  device.outputAVState = 1
  device.isDisabled = false
  device.lastOutputTorque = 0

  device.loadSmoother:reset()
  device.throttleSmoother:reset()
  device.engineLoad = 0
  device.instantEngineLoad = 0
  device.ignitionCutTime = 0
  device.slowIgnitionErrorCoef = 1
  device.fastIgnitionErrorCoef = 1

  device.sustainedAfterFireTimer = 0
  device.instantAfterFireFuel = 0
  device.sustainedAfterFireFuel = 0
  device.shiftAfterFireFuel = 0
  device.continuousAfterFireFuel = 0
  device.instantAfterFireFuelDelay:reset()
  device.sustainedAfterFireFuelDelay:reset()

  device.overRevDamage = 0
  device.overTorqueDamage = 0

  device.engineWorkPerUpdate = 0
  device.frictionLossPerUpdate = 0
  device.pumpingLossPerUpdate = 0
  device.spentEnergy = 0
  device.spentEnergyNitrousOxide = 0
  device.storageWithEnergyCounter = 0
  device.registeredEnergyStorages = {}
  device.previousEnergyLevels = {}
  device.energyStorageRatios = {}
  device.hasFuel = true
  device.remainingFuelRatio = 1

  device.revLimiterActive = false
  device.revLimiterWasActiveTimer = 999

  device.brakeSpecificFuelConsumption = 0

  device.wearFrictionCoef = 1
  device.damageFrictionCoef = 1
  device.wearDynamicFrictionCoef = 1
  device.damageDynamicFrictionCoef = 1
  device.wearIdleAVReadErrorRangeCoef = 1
  device.damageIdleAVReadErrorRangeCoef = 1

  device:resetTempRevLimiter()

  device.thermals.reset(jbeamData)

  device.turbocharger.reset(v.data[jbeamData.turbocharger])
  device.supercharger.reset(v.data[jbeamData.supercharger])
  device.nitrousOxideInjection.reset(jbeamData)

  device.torqueData = getTorqueData(device)
  device.maxPower = device.torqueData.maxPower
  device.maxTorque = device.torqueData.maxTorque
  device.maxPowerThrottleMap = device.torqueData.maxPower * psToWatt

  damageTracker.setDamage("engine", "engineDisabled", false)
  damageTracker.setDamage("engine", "engineLockedUp", false)
  damageTracker.setDamage("engine", "engineReducedTorque", false)
  damageTracker.setDamage("engine", "catastrophicOverrevDamage", false)
  damageTracker.setDamage("engine", "mildOverrevDamage", false)
  damageTracker.setDamage("engine", "overRevDanger", false)
  damageTracker.setDamage("engine", "catastrophicOverTorqueDamage", false)
  damageTracker.setDamage("engine", "overTorqueDanger", false)
  damageTracker.setDamage("engine", "engineHydrolocked", false)
  damageTracker.setDamage("engine", "engineIsHydrolocking", false)
  damageTracker.setDamage("engine", "impactDamage", false)

  selectUpdates(device)
end

local function initSounds(device, jbeamData)
  local exhaustEndNodes = device.thermals.exhaustEndNodes or {}

  device.engineMiscSounds = {
    starterSoundEngine = obj:createSFXSource2(jbeamData.starterSample or "event:>Engine>Starter>Old_V2", "AudioDefaultLoop3D", "", device.engineNodeID, 0),
    starterVolume = jbeamData.starterVolume or 1,
    starterVolumeExhaust = jbeamData.starterVolumeExhaust or 1,
    shutOffVolumeEngine = jbeamData.shutOffVolumeEngine or 1,
    shutOffVolumeExhaust = jbeamData.shutOffVolumeExhaust or 1
  }
  obj:setVolume(device.engineMiscSounds.starterSoundEngine, device.engineMiscSounds.starterVolume)

  if jbeamData.starterSampleExhaust then
    local starterExhaustNode = #exhaustEndNodes > 0 and exhaustEndNodes[1].finish or device.engineNodeID
    device.engineMiscSounds.starterSoundExhaust = obj:createSFXSource2(jbeamData.starterSampleExhaust, "AudioDefaultLoop3D", "", starterExhaustNode, 0)
    obj:setVolume(device.engineMiscSounds.starterSoundExhaust, device.engineMiscSounds.starterVolumeExhaust)
  end

  if jbeamData.shutOffSampleEngine then
    local shutOffEngineNode = device.engineNodeID or 0
    device.engineMiscSounds.shutOffSoundEngine = obj:createSFXSource2(jbeamData.shutOffSampleEngine, "AudioDefaultLoop3D", "", shutOffEngineNode, 0)
    obj:setVolume(device.engineMiscSounds.shutOffSoundEngine, device.engineMiscSounds.shutOffVolumeEngine)
  end

  if jbeamData.shutOffSampleExhaust then
    local shutOffExhaustNode = #exhaustEndNodes > 0 and exhaustEndNodes[1].finish or device.engineNodeID
    device.engineMiscSounds.shutOffSoundExhaust = obj:createSFXSource2(jbeamData.shutOffSampleExhaust, "AudioDefaultLoop3D", "", shutOffExhaustNode, 0)
    obj:setVolume(device.engineMiscSounds.shutOffSoundExhaust, device.engineMiscSounds.shutOffVolumeExhaust)
  end

  if not sounds.usesOldCustomSounds then
    local hasNewSounds = false
    if jbeamData.soundConfig then
      device.soundConfiguration = {}
      local soundConfig = v.data[jbeamData.soundConfig]

      if soundConfig then
        device.engineSoundID = powertrain.getEngineSoundID()
        device.soundMaxLoadMix = soundConfig.maxLoadMix or 1
        device.soundMinLoadMix = soundConfig.minLoadMix or 0
        local onLoadGain = soundConfig.onLoadGain or 1
        local offLoadGain = soundConfig.offLoadGain or 1
        local fundamentalFrequencyCylinderCount = soundConfig.fundamentalFrequencyCylinderCount or 6
        device.engineVolumeCoef = 1

        local sampleName = soundConfig.sampleName
        local sampleFolder = soundConfig.sampleFolder or "art/sound/blends/"
        local samplePath = sampleFolder .. sampleName .. ".sfxBlend2D.json"

        local engineNodeIDs = {device.engineNodeID} --Hardcode intake sound location to a single node, no need for multiple
        device:initEngineSound(device.engineSoundID, samplePath, engineNodeIDs, offLoadGain, onLoadGain, "engine")

        local main_gain = soundConfig.mainGain or 0

        local eq_a_freq = sounds.hzToFMODHz(soundConfig.lowShelfFreq or soundConfig.lowCutFreq or 20)
        local eq_a_gain = soundConfig.lowShelfGain or 0
        local eq_b_freq = sounds.hzToFMODHz(soundConfig.highShelfFreq or soundConfig.highCutFreq or 10000)
        local eq_b_gain = soundConfig.highShelfGain or 0
        local eq_c_freq = sounds.hzToFMODHz(soundConfig.eqLowFreq or 500)
        local eq_c_gain = soundConfig.eqLowGain or 0
        local eq_c_reso = soundConfig.eqLowWidth or 0
        local eq_d_freq = sounds.hzToFMODHz(soundConfig.eqHighFreq or 2000)
        local eq_d_gain = soundConfig.eqHighGain or 0
        local eq_d_reso = soundConfig.eqHighWidth or 0
        local eq_e_gain = soundConfig.eqFundamentalGain or 0

        local enginePlacement = jbeamData.enginePlacement or "outside"
        local c_enginePlacement = 0
        if enginePlacement == "outside" then
          c_enginePlacement = 0
        elseif enginePlacement == "inside" then
          c_enginePlacement = 1
        end

        local intakeMuffling = soundConfig.intakeMuffling or 1

        -- print (string.format("       ENGINE idleRPM = %4.0f / maxRPM = %5.0f", jbeamData.idleRPM, jbeamData.maxRPM))
        -- print (string.format("       ENGINE idleRPM = %4.0f / limiterRPM = %5.0f / maxRPM = %5.0f", jbeamData.idleRPM, jbeamData.revLimiterRPM, jbeamData.maxRPM))
        -- print (string.format("%s  / maingain %4.2fdB / Muffling %.2f / lowShelf %.0f %4.2fdB / highShelf %4.0f %.2fdB / eqLow %.0f %.2fdB/ eqHigh %4.0f %.2fdB / eqFundamental %.2fdB", sampleName, main_gain, intakeMuffling, eq_a_freq, eq_a_gain, eq_b_freq, eq_b_gain, eq_c_freq, eq_c_gain, eq_d_freq, eq_d_gain, eq_e_gain))

        local params = {
          base_gain = main_gain,
          main_gain = 0,
          eq_a_freq = eq_a_freq,
          eq_a_gain = eq_a_gain,
          eq_b_freq = eq_b_freq,
          eq_b_gain = eq_b_gain,
          eq_c_freq = eq_c_freq,
          eq_c_gain = eq_c_gain,
          eq_c_reso = eq_c_reso,
          eq_d_freq = eq_d_freq,
          eq_d_gain = eq_d_gain,
          eq_d_reso = eq_d_reso,
          eq_e_gain = eq_e_gain,
          onLoadGain = onLoadGain,
          offLoadGain = offLoadGain,
          base_muffled = intakeMuffling,
          muffled = 0,
          gainOffset = 0,
          mufflingOffset = 0,
          mufflingOffsetRevLimiter = 0,
          gainOffsetRevLimiter = 0,
          fundamentalFrequencyRPMCoef = fundamentalFrequencyCylinderCount / 120,
          c_enginePlacement = c_enginePlacement
        }
        --dump(params)
        device:setEngineSoundParameterList(device.engineSoundID, params, "engine")
        --dump(sounds)
        hasNewSounds = true
      else
        log("E", "combustionEngine.init", "Can't find sound config: " .. jbeamData.soundConfig)
      end
    end
    if jbeamData.soundConfigExhaust then
      device.soundConfiguration = device.soundConfiguration or {}
      local soundConfig = v.data[jbeamData.soundConfigExhaust]
      if soundConfig then
        device.engineSoundIDExhaust = powertrain.getEngineSoundID()
        device.soundMaxLoadMixExhaust = soundConfig.maxLoadMix
        device.soundMinLoadMixExhaust = soundConfig.minLoadMix
        local onLoadGain = soundConfig.onLoadGain or 1
        local offLoadGain = soundConfig.offLoadGain or 1
        local fundamentalFrequencyCylinderCount = soundConfig.fundamentalFrequencyCylinderCount or 6
        device.engineVolumeCoef = 1

        local sampleName = soundConfig.sampleName
        local sampleFolder = soundConfig.sampleFolder or "art/sound/blends/"
        local samplePath = sampleFolder .. sampleName .. ".sfxBlend2D.json"

        local endNodeIDPairs

        device.exhaustAudioMufflingMinCoef = soundConfig.exhaustAudioMufflingBaseCoef or 0
        device.exhaustAudioMufflingCoefRange = 1 - device.exhaustAudioMufflingMinCoef
        local maxExhaustAudioOpennessCoef = 0
        local maxExhaustAudioGain
        if #exhaustEndNodes > 0 then
          endNodeIDPairs = {}
          for _, v in pairs(exhaustEndNodes) do
            maxExhaustAudioOpennessCoef = min(max(maxExhaustAudioOpennessCoef, v.exhaustAudioOpennessCoef), 1)
            maxExhaustAudioGain = maxExhaustAudioGain and max(maxExhaustAudioGain, v.exhaustAudioGainChange) or v.exhaustAudioGainChange --we want the biggest number, ie the least amount of muffling
            table.insert(endNodeIDPairs, {v.start, v.finish})
          end
        else
          endNodeIDPairs = {{device.engineNodeID, device.engineNodeID}}
          maxExhaustAudioGain = 0
        end
        device:initExhaustSound(device.engineSoundIDExhaust, samplePath, endNodeIDPairs, offLoadGain, onLoadGain, "exhaust")

        device.exhaustMainGain = soundConfig.mainGain or 0
        local main_gain = device.exhaustMainGain + maxExhaustAudioGain

        local eq_a_freq = sounds.hzToFMODHz(soundConfig.lowShelfFreq or soundConfig.lowCutFreq or 20)
        local eq_a_gain = soundConfig.lowShelfGain or 0
        local eq_b_freq = sounds.hzToFMODHz(soundConfig.highShelfFreq or soundConfig.highCutFreq or 10000)
        local eq_b_gain = soundConfig.highShelfGain or 0
        local eq_c_freq = sounds.hzToFMODHz(soundConfig.eqLowFreq or 500)
        local eq_c_gain = soundConfig.eqLowGain or 0
        local eq_c_reso = soundConfig.eqLowWidth or 0
        local eq_d_freq = sounds.hzToFMODHz(soundConfig.eqHighFreq or 2000)
        local eq_d_gain = soundConfig.eqHighGain or 0
        local eq_d_reso = soundConfig.eqHighWidth or 0
        local eq_e_gain = soundConfig.eqFundamentalGain or 0

        local exhaustMuffling = device.exhaustAudioMufflingMinCoef + device.exhaustAudioMufflingCoefRange * (1 - maxExhaustAudioOpennessCoef)

        -- print (string.format("%s / maingain %4.2fdB / Muffling %.2f / lowShelf %.0fhz %4.2fdB / highShelf %4.0fhz %.2fdB / eqLow %.0fhz %.2fdB/ eqHigh %4.0fhz %.2fdB / eqFundamental %.2fdB ",sampleName, main_gain, exhaustMuffling, eq_a_freq, eq_a_gain, eq_b_freq, eq_b_gain, eq_c_freq, eq_c_gain, eq_d_freq, eq_d_gain, eq_e_gain))

        local params = {
          base_gain = main_gain,
          main_gain = 0,
          eq_a_freq = eq_a_freq,
          eq_a_gain = eq_a_gain,
          eq_b_freq = eq_b_freq,
          eq_b_gain = eq_b_gain,
          eq_c_freq = eq_c_freq,
          eq_c_gain = eq_c_gain,
          eq_c_reso = eq_c_reso,
          eq_d_freq = eq_d_freq,
          eq_d_gain = eq_d_gain,
          eq_d_reso = eq_d_reso,
          eq_e_gain = eq_e_gain,
          onLoadGain = onLoadGain,
          offLoadGain = offLoadGain,
          base_muffled = exhaustMuffling,
          muffled = 0,
          gainOffset = 0,
          mufflingOffset = 0,
          mufflingOffsetRevLimiter = 0,
          gainOffsetRevLimiter = 0,
          fundamentalFrequencyRPMCoef = fundamentalFrequencyCylinderCount / 120
        }
        --dump(params)

        device:setEngineSoundParameterList(device.engineSoundIDExhaust, params, "exhaust")
        hasNewSounds = true
      else
        log("E", "combustionEngine.init", "Can't find sound config: " .. jbeamData.soundConfigExhaust)
      end
    end

    if hasNewSounds then
      local rpmInRate = jbeamData.rpmSmootherInRate or 15
      local rpmOutRate = jbeamData.rpmSmootherOutRate or 25
      device.soundRPMSmoother = newTemporalSmoothingNonLinear(rpmInRate, rpmOutRate)
      local loadInRate = jbeamData.loadSmootherInRate or 20
      local loadOutRate = jbeamData.loadSmootherOutRate or 20
      device.soundLoadSmoother = newTemporalSmoothingNonLinear(loadInRate, loadOutRate)

      device.updateSounds = updateSounds
      sounds.disableOldEngineSounds()
    end
  else
    log("W", "combustionEngine.initSounds", "Disabling new sounds, found old custom engine sounds...")
  end

  device.turbocharger.initSounds(v.data[jbeamData.turbocharger])
  device.supercharger.initSounds(v.data[jbeamData.supercharger])
  device.nitrousOxideInjection.initSounds(v.data[jbeamData.nitrousOxideInjection])
  device.thermals.initSounds(jbeamData)
end

local function new(jbeamData)
  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,
    friction = jbeamData.friction or 0,
    cumulativeInertia = 1,
    cumulativeGearRatio = 1,
    maxCumulativeGearRatio = 1,
    isPhysicallyDisconnected = true,
    isPropulsed = true,
    inputAV = 0,
    outputTorque1 = 0,
    virtualMassAV = 0,
    isBroken = false,
    combustionTorque = 0,
    frictionTorque = 0,
    nitrousOxideTorque = 0,
    electricsThrottleName = jbeamData.electricsThrottleName or "throttle",
    electricsThrottleFactorName = jbeamData.electricsThrottleFactorName or "throttleFactor",
    throttleFactor = 1,
    throttle = 0,
    requestedThrottle = 0,
    dynamicFriction = jbeamData.dynamicFriction or 0,
    idleRPM = jbeamData.idleRPM,
    idleAV = jbeamData.idleRPM * rpmToAV,
    idleAVOverwrite = 0,
    maxRPM = jbeamData.maxRPM,
    maxAV = jbeamData.maxRPM * rpmToAV,
    idleAVReadError = 0,
    idleAVReadErrorRange = (jbeamData.idleRPMRoughness or 50) * rpmToAV,
    inertia = jbeamData.inertia or 0.1,
    idleAVStartOffset = 0,
    maxIdleThrottle = jbeamData.maxIdleThrottle or 0.15,
    maxIdleThrottleOverwrite = 0,
    starterTorque = jbeamData.starterTorque or (jbeamData.friction * 15),
    starterMaxAV = (jbeamData.starterMaxRPM or jbeamData.idleRPM * 0.7) * rpmToAV,
    shutOffSoundRequested = false,
    starterEngagedCoef = 0,
    starterThrottleKillCoef = 1,
    starterThrottleKillTimer = 0,
    starterThrottleKillTime = jbeamData.starterThrottleKillTime or 0.5,
    starterDisabled = false,
    stallTimer = 1,
    isStalled = false,
    floodLevel = 0,
    prevFloodPercent = 0,
    particulates = jbeamData.particulates,
    thermalsEnabled = jbeamData.thermalsEnabled,
    engineBlockMaterial = jbeamData.engineBlockMaterial,
    oilVolume = jbeamData.oilVolume,
    cylinderWallTemperatureDamageThreshold = jbeamData.cylinderWallTemperatureDamageThreshold,
    headGasketDamageThreshold = jbeamData.headGasketDamageThreshold,
    pistonRingDamageThreshold = jbeamData.pistonRingDamageThreshold,
    connectingRodDamageThreshold = jbeamData.connectingRodDamageThreshold,
    forcedInductionCoef = 1,
    intakeAirDensityCoef = 1,
    outputTorqueState = 1,
    outputAVState = 1,
    isDisabled = false,
    lastOutputTorque = 0,
    loadSmoother = newTemporalSmoothing(2, 2),
    throttleSmoother = newTemporalSmoothing(15, 10),
    engineLoad = 0,
    instantEngineLoad = 0,
    ignitionCutTime = 0,
    slowIgnitionErrorCoef = 1,
    fastIgnitionErrorCoef = 1,
    instantAfterFireCoef = jbeamData.instantAfterFireCoef or 0,
    sustainedAfterFireCoef = jbeamData.sustainedAfterFireCoef or 0,
    sustainedAfterFireTimer = 0,
    sustainedAfterFireTime = jbeamData.sustainedAfterFireTime or 1.5,
    instantAfterFireFuel = 0,
    sustainedAfterFireFuel = 0,
    shiftAfterFireFuel = 0,
    continuousAfterFireFuel = 0,
    instantAfterFireFuelDelay = delayLine.new(0.1),
    sustainedAfterFireFuelDelay = delayLine.new(0.3),
    exhaustFlowDelay = delayLine.new(0.1),
    overRevDamage = 0,
    maxOverRevDamage = jbeamData.maxOverRevDamage or 1500,
    maxTorqueRating = jbeamData.maxTorqueRating or -1,
    overTorqueDamage = 0,
    maxOverTorqueDamage = jbeamData.maxOverTorqueDamage or 1000,
    engineWorkPerUpdate = 0,
    frictionLossPerUpdate = 0,
    pumpingLossPerUpdate = 0,
    spentEnergy = 0,
    spentEnergyNitrousOxide = 0,
    storageWithEnergyCounter = 0,
    registeredEnergyStorages = {},
    previousEnergyLevels = {},
    energyStorageRatios = {},
    hasFuel = true,
    remainingFuelRatio = 1,
    fixedStepTimer = 0,
    fixedStepTime = 1 / 100,
	
	idleRPMVariance = jbeamData.idleRPMVariance or 3000, -- ZEIT EDIT
    idleRPMVarianceFrequency = jbeamData.idleRPMVarianceFrequency or 3, -- ZEIT EDIT
    idleRPMSmoother = newLinearSmoothing(physicsDt*1000), -- ZEIT EDIT
    idleAVTimer = 0, -- ZEIT EDIT
    idleAVVariance = 0, -- ZEIT EDIT
    --
    --wear/damage modifiers
    wearFrictionCoef = 1,
    damageFrictionCoef = 1,
    wearDynamicFrictionCoef = 1,
    damageDynamicFrictionCoef = 1,
    wearIdleAVReadErrorRangeCoef = 1,
    damageIdleAVReadErrorRangeCoef = 1,
    --
    --methods
    initSounds = initSounds,
    resetSounds = resetSounds,
    setExhaustGainMufflingOffset = setExhaustGainMufflingOffset,
    setExhaustGainMufflingOffsetRevLimiter = setExhaustGainMufflingOffsetRevLimiter,
    reset = reset,
    onBreak = onBreak,
    beamBroke = beamBroke,
    validate = validate,
    calculateInertia = calculateInertia,
    updateGFX = updateGFX,
    updateFixedStep = updateFixedStep,
    updateSounds = nil,
    scaleFriction = scaleFriction,
    scaleFrictionInitial = scaleFrictionInitial,
    scaleOutputTorque = scaleOutputTorque,
    activateStarter = activateStarter,
    deactivateStarter = deactivateStarter,
    sendTorqueData = sendTorqueData,
    getTorqueData = getTorqueData,
    checkHydroLocking = checkHydroLocking,
    lockUp = lockUp,
    disable = disable,
    enable = enable,
    setIgnition = setIgnition,
    cutIgnition = cutIgnition,
    setTempRevLimiter = setTempRevLimiter,
    resetTempRevLimiter = resetTempRevLimiter,
    updateFuelUsage = updateFuelUsage,
    updateEnergyStorageRatios = updateEnergyStorageRatios,
    registerStorage = registerStorage,
    exhaustEndNodesChanged = exhaustEndNodesChanged,
    initEngineSound = initEngineSound,
    initExhaustSound = initExhaustSound,
    setEngineSoundParameterList = setEngineSoundParameterList,
    getSoundConfiguration = getSoundConfiguration,
    applyDeformGroupDamage = applyDeformGroupDamage,
    setPartCondition = setPartCondition,
    getPartCondition = getPartCondition
  }

  device.spawnVehicleIgnitionLevel = electrics.values.ignitionLevel
  local spawnWithEngineRunning = device.spawnVehicleIgnitionLevel > 2
  local spawnWithIgnitionOn = device.spawnVehicleIgnitionLevel > 1

  --this code handles the requirement to support multiple output clutches
  --by default the engine has only one output, we need to know the number before building the tree, so it needs to be specified in jbeam
  device.numberOfOutputPorts = jbeamData.numberOfOutputPorts or 1
  device.outputPorts = {} --reset the defined outputports
  device.outputTorqueNames = {}
  device.outputAVNames = {}
  for i = 1, device.numberOfOutputPorts, 1 do
    device.outputPorts[i] = true --let powertrain know which outputports we support
    --cache the required output torque and AV property names for fast access
    device.outputTorqueNames[i] = "outputTorque" .. tostring(i)
    device.outputAVNames[i] = "outputAV" .. tostring(i)
    device[device.outputTorqueNames[i]] = 0
    device[device.outputAVNames[i]] = spawnWithEngineRunning and (jbeamData.idleRPM * rpmToAV) or 0
  end

  device.outputRPM = device.outputAV1 * avToRPM
  device.lastOutputAV1 = device.outputAV1
  device.ignitionCoef = spawnWithIgnitionOn and 1 or 0

  device.initialFriction = device.friction
  device.engineBrakeTorque = jbeamData.engineBrakeTorque or device.friction * 2

  local torqueReactionNodes_nodes = jbeamData.torqueReactionNodes_nodes
  if torqueReactionNodes_nodes and type(torqueReactionNodes_nodes) == "table" then
    local hasValidReactioNodes = true
    for _, v in pairs(torqueReactionNodes_nodes) do
      if type(v) ~= "number" then
        hasValidReactioNodes = false
      end
    end
    if hasValidReactioNodes then
      device.torqueReactionNodes = torqueReactionNodes_nodes
    end
  end
  if not device.torqueReactionNodes then
    device.torqueReactionNodes = {-1, -1, -1}
  end

  device.waterDamageNodes = jbeamData.waterDamage and jbeamData.waterDamage._engineGroup_nodes or {}

  device.canFlood = device.waterDamageNodes and type(device.waterDamageNodes) == "table" and #device.waterDamageNodes > 0

  device.maxPhysicalAV = (jbeamData.maxPhysicalRPM or (jbeamData.maxRPM * 1.05)) * rpmToAV --what the engine is physically capable of

  if not jbeamData.torque then
    log("E", "combustionEngine.init", "Can't find torque table... Powertrain is going to break!")
  end

  local baseTorqueTable = tableFromHeaderTable(jbeamData.torque)
  local rawBasePoints = {}
  local maxAvailableRPM = 0
  for _, v in pairs(baseTorqueTable) do
    maxAvailableRPM = max(maxAvailableRPM, v.rpm)
    table.insert(rawBasePoints, {v.rpm, v.torque})
    -- print (string.format("RPM = %5.0f, TORQUE = %4.0f", v.rpm, v.torque))
  end
  local rawBaseCurve = createCurve(rawBasePoints)

  local rawTorqueMultCurve = {}
  if jbeamData.torqueModMult then
    local multTorqueTable = tableFromHeaderTable(jbeamData.torqueModMult)
    local rawTorqueMultPoints = {}
    for _, v in pairs(multTorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawTorqueMultPoints, {v.rpm, v.torque})
    end
    rawTorqueMultCurve = createCurve(rawTorqueMultPoints)
  end

  local rawIntakeCurve = {}
  local lastRawIntakeValue = 0
  if jbeamData.torqueModIntake then
    local intakeTorqueTable = tableFromHeaderTable(jbeamData.torqueModIntake)
    local rawIntakePoints = {}
    for _, v in pairs(intakeTorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawIntakePoints, {v.rpm, v.torque})
    end
    rawIntakeCurve = createCurve(rawIntakePoints)
    lastRawIntakeValue = rawIntakeCurve[#rawIntakeCurve]
  end

  local rawExhaustCurve = {}
  local lastRawExhaustValue = 0
  if jbeamData.torqueModExhaust then
    local exhaustTorqueTable = tableFromHeaderTable(jbeamData.torqueModExhaust)
    local rawExhaustPoints = {}
    for _, v in pairs(exhaustTorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawExhaustPoints, {v.rpm, v.torque})
    end
    rawExhaustCurve = createCurve(rawExhaustPoints)
    lastRawExhaustValue = rawExhaustCurve[#rawExhaustCurve]
  end



local rawPart1Curve = {}
  local lastRawPart1Value = 0
  if jbeamData.torqueModPart1 then
    local Part1TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart1)
    local rawPart1Points = {}
    for _, v in pairs(Part1TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart1Points, {v.rpm, v.torque})
    end
    rawPart1Curve = createCurve(rawPart1Points)
    lastRawPart1Value = rawPart1Curve[#rawPart1Curve]
  end
  
  
  local rawPart2Curve = {}
  local lastRawPart2Value = 0
  if jbeamData.torqueModPart2 then
    local Part2TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart2)
    local rawPart2Points = {}
    for _, v in pairs(Part2TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart2Points, {v.rpm, v.torque})
    end
    rawPart2Curve = createCurve(rawPart2Points)
    lastRawPart2Value = rawPart2Curve[#rawPart2Curve]
  end
  
  local rawPart3Curve = {}
  local lastRawPart3Value = 0
  if jbeamData.torqueModPart3 then
    local Part3TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart3)
    local rawPart3Points = {}
    for _, v in pairs(Part3TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart3Points, {v.rpm, v.torque})
    end
    rawPart3Curve = createCurve(rawPart3Points)
    lastRawPart3Value = rawPart3Curve[#rawPart3Curve]
  end
  
  local rawPart4Curve = {}
  local lastRawPart4Value = 0
  if jbeamData.torqueModPart4 then
    local Part4TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart4)
    local rawPart4Points = {}
    for _, v in pairs(Part4TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart4Points, {v.rpm, v.torque})
    end
    rawPart4Curve = createCurve(rawPart4Points)
    lastRawPart4Value = rawPart4Curve[#rawPart4Curve]
  end
  
  local rawPart5Curve = {}
  local lastRawPart5Value = 0
  if jbeamData.torqueModPart5 then
    local Part5TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart5)
    local rawPart5Points = {}
    for _, v in pairs(Part5TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart5Points, {v.rpm, v.torque})
    end
    rawPart5Curve = createCurve(rawPart5Points)
    lastRawPart5Value = rawPart5Curve[#rawPart5Curve]
  end
  
  local rawPart6Curve = {}
  local lastRawPart6Value = 0
  if jbeamData.torqueModPart6 then
    local Part6TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart6)
    local rawPart6Points = {}
    for _, v in pairs(Part6TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart6Points, {v.rpm, v.torque})
    end
    rawPart6Curve = createCurve(rawPart6Points)
    lastRawPart6Value = rawPart6Curve[#rawPart6Curve]
  end
  
  local rawPart7Curve = {}
  local lastRawPart7Value = 0
  if jbeamData.torqueModPart7 then
    local Part7TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart7)
    local rawPart7Points = {}
    for _, v in pairs(Part7TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart7Points, {v.rpm, v.torque})
    end
    rawPart7Curve = createCurve(rawPart7Points)
    lastRawPart7Value = rawPart7Curve[#rawPart7Curve]
  end
  
  local rawPart8Curve = {}
  local lastRawPart8Value = 0
  if jbeamData.torqueModPart8 then
    local Part8TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart8)
    local rawPart8Points = {}
    for _, v in pairs(Part8TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart8Points, {v.rpm, v.torque})
    end
    rawPart8Curve = createCurve(rawPart8Points)
    lastRawPart8Value = rawPart8Curve[#rawPart8Curve]
  end
  
  local rawPart9Curve = {}
  local lastRawPart9Value = 0
  if jbeamData.torqueModPart9 then
    local Part9TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart9)
    local rawPart9Points = {}
    for _, v in pairs(Part9TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart9Points, {v.rpm, v.torque})
    end
    rawPart9Curve = createCurve(rawPart9Points)
    lastRawPart9Value = rawPart9Curve[#rawPart9Curve]
  end
  
  local rawPart10Curve = {}
  local lastRawPart10Value = 0
  if jbeamData.torqueModPart10 then
    local Part10TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart10)
    local rawPart10Points = {}
    for _, v in pairs(Part10TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart10Points, {v.rpm, v.torque})
    end
    rawPart10Curve = createCurve(rawPart10Points)
    lastRawPart10Value = rawPart10Curve[#rawPart10Curve]
  end
  
  local rawPart11Curve = {}
  local lastRawPart11Value = 0
  if jbeamData.torqueModPart11 then
    local Part11TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart11)
    local rawPart11Points = {}
    for _, v in pairs(Part11TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart11Points, {v.rpm, v.torque})
    end
    rawPart11Curve = createCurve(rawPart11Points)
    lastRawPart11Value = rawPart11Curve[#rawPart11Curve]
  end
  
  local rawPart12Curve = {}
  local lastRawPart12Value = 0
  if jbeamData.torqueModPart12 then
    local Part12TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart12)
    local rawPart12Points = {}
    for _, v in pairs(Part12TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart12Points, {v.rpm, v.torque})
    end
    rawPart12Curve = createCurve(rawPart12Points)
    lastRawPart12Value = rawPart12Curve[#rawPart12Curve]
  end
  
  local rawPart13Curve = {}
  local lastRawPart13Value = 0
  if jbeamData.torqueModPart13 then
    local Part13TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart13)
    local rawPart13Points = {}
    for _, v in pairs(Part13TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart13Points, {v.rpm, v.torque})
    end
    rawPart13Curve = createCurve(rawPart13Points)
    lastRawPart13Value = rawPart13Curve[#rawPart13Curve]
  end
  
  local rawPart14Curve = {}
  local lastRawPart14Value = 0
  if jbeamData.torqueModPart14 then
    local Part14TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart14)
    local rawPart14Points = {}
    for _, v in pairs(Part14TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart14Points, {v.rpm, v.torque})
    end
    rawPart14Curve = createCurve(rawPart14Points)
    lastRawPart14Value = rawPart14Curve[#rawPart14Curve]
  end
  
  local rawPart15Curve = {}
  local lastRawPart15Value = 0
  if jbeamData.torqueModPart15 then
    local Part15TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart15)
    local rawPart15Points = {}
    for _, v in pairs(Part15TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart15Points, {v.rpm, v.torque})
    end
    rawPart15Curve = createCurve(rawPart15Points)
    lastRawPart15Value = rawPart15Curve[#rawPart15Curve]
  end
  
  local rawPart16Curve = {}
  local lastRawPart16Value = 0
  if jbeamData.torqueModPart16 then
    local Part16TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart16)
    local rawPart16Points = {}
    for _, v in pairs(Part16TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart16Points, {v.rpm, v.torque})
    end
    rawPart16Curve = createCurve(rawPart16Points)
    lastRawPart16Value = rawPart16Curve[#rawPart16Curve]
  end
  
  local rawPart17Curve = {}
  local lastRawPart17Value = 0
  if jbeamData.torqueModPart17 then
    local Part17TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart17)
    local rawPart17Points = {}
    for _, v in pairs(Part17TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart17Points, {v.rpm, v.torque})
    end
    rawPart17Curve = createCurve(rawPart17Points)
    lastRawPart17Value = rawPart17Curve[#rawPart17Curve]
  end
  
  local rawPart18Curve = {}
  local lastRawPart18Value = 0
  if jbeamData.torqueModPart18 then
    local Part18TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart18)
    local rawPart18Points = {}
    for _, v in pairs(Part18TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart18Points, {v.rpm, v.torque})
    end
    rawPart18Curve = createCurve(rawPart18Points)
    lastRawPart18Value = rawPart18Curve[#rawPart18Curve]
  end
  
  local rawPart19Curve = {}
  local lastRawPart19Value = 0
  if jbeamData.torqueModPart19 then
    local Part19TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart19)
    local rawPart19Points = {}
    for _, v in pairs(Part19TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart19Points, {v.rpm, v.torque})
    end
    rawPart19Curve = createCurve(rawPart19Points)
    lastRawPart19Value = rawPart19Curve[#rawPart19Curve]
  end
  
  local rawPart20Curve = {}
  local lastRawPart20Value = 0
  if jbeamData.torqueModPart20 then
    local Part20TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart20)
    local rawPart20Points = {}
    for _, v in pairs(Part20TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart20Points, {v.rpm, v.torque})
    end
    rawPart20Curve = createCurve(rawPart20Points)
    lastRawPart20Value = rawPart20Curve[#rawPart20Curve]
  end
  
  local rawPart21Curve = {}
  local lastRawPart21Value = 0
  if jbeamData.torqueModPart21 then
    local Part21TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart21)
    local rawPart21Points = {}
    for _, v in pairs(Part21TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart21Points, {v.rpm, v.torque})
    end
    rawPart21Curve = createCurve(rawPart21Points)
    lastRawPart21Value = rawPart21Curve[#rawPart21Curve]
  end
  
  local rawPart22Curve = {}
  local lastRawPart22Value = 0
  if jbeamData.torqueModPart22 then
    local Part22TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart22)
    local rawPart22Points = {}
    for _, v in pairs(Part22TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart22Points, {v.rpm, v.torque})
    end
    rawPart22Curve = createCurve(rawPart22Points)
    lastRawPart22Value = rawPart22Curve[#rawPart22Curve]
  end
  
  local rawPart23Curve = {}
  local lastRawPart23Value = 0
  if jbeamData.torqueModPart23 then
    local Part23TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart23)
    local rawPart23Points = {}
    for _, v in pairs(Part23TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart23Points, {v.rpm, v.torque})
    end
    rawPart23Curve = createCurve(rawPart23Points)
    lastRawPart23Value = rawPart23Curve[#rawPart23Curve]
  end
  
    local rawPart24Curve = {}
  local lastRawPart24Value = 0
  if jbeamData.torqueModPart24 then
    local Part24TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart24)
    local rawPart24Points = {}
    for _, v in pairs(Part24TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart24Points, {v.rpm, v.torque})
    end
    rawPart24Curve = createCurve(rawPart24Points)
    lastRawPart24Value = rawPart24Curve[#rawPart24Curve]
  end
  
   local rawPart25Curve = {}
  local lastRawPart25Value = 0
  if jbeamData.torqueModPart25 then
    local Part25TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart25)
    local rawPart25Points = {}
    for _, v in pairs(Part25TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart25Points, {v.rpm, v.torque})
    end
    rawPart25Curve = createCurve(rawPart25Points)
    lastRawPart25Value = rawPart25Curve[#rawPart25Curve]
  end
  
   local rawPart26Curve = {}
  local lastRawPart26Value = 0
  if jbeamData.torqueModPart26 then
    local Part26TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart26)
    local rawPart26Points = {}
    for _, v in pairs(Part26TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart26Points, {v.rpm, v.torque})
    end
    rawPart26Curve = createCurve(rawPart26Points)
    lastRawPart26Value = rawPart26Curve[#rawPart26Curve]
  end
  
     local rawPart27Curve = {}
  local lastRawPart27Value = 0
  if jbeamData.torqueModPart27 then
    local Part27TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart27)
    local rawPart27Points = {}
    for _, v in pairs(Part27TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart26Points, {v.rpm, v.torque})
    end
    rawPart27Curve = createCurve(rawPart27Points)
    lastRawPart27Value = rawPart27Curve[#rawPart27Curve]
  end
  
  local rawPart28Curve = {}
  local lastRawPart28Value = 0
  if jbeamData.torqueModPart28 then
    local Part28TorqueTable = tableFromHeaderTable(jbeamData.torqueModPart28)
    local rawPart28Points = {}
    for _, v in pairs(Part27TorqueTable) do
      maxAvailableRPM = max(maxAvailableRPM, v.rpm)
      table.insert(rawPart28Points, {v.rpm, v.torque})
    end
    rawPart28Curve = createCurve(rawPart28Points)
    lastRawPart28Value = rawPart28Curve[#rawPart28Curve]
  end




  local rawCombinedCurve = {}
  for i = 0, maxAvailableRPM, 1 do
    local base = rawBaseCurve[i] or 0
    local baseMult = rawTorqueMultCurve[i] or 1
    local intake = rawIntakeCurve[i] or lastRawIntakeValue
	local Part1 = rawPart1Curve[i] or lastRawPart1Value
	local Part2 = rawPart2Curve[i] or lastRawPart2Value
	local Part3 = rawPart3Curve[i] or lastRawPart3Value
	local Part4 = rawPart4Curve[i] or lastRawPart4Value
	local Part5 = rawPart5Curve[i] or lastRawPart5Value
	local Part6 = rawPart6Curve[i] or lastRawPart6Value
	local Part7 = rawPart7Curve[i] or lastRawPart7Value
	local Part8 = rawPart8Curve[i] or lastRawPart8Value
	local Part9 = rawPart9Curve[i] or lastRawPart9Value
	local Part10 = rawPart10Curve[i] or lastRawPart10Value
	local Part11 = rawPart11Curve[i] or lastRawPart11Value
	local Part12 = rawPart12Curve[i] or lastRawPart12Value
	local Part13 = rawPart13Curve[i] or lastRawPart13Value
	local Part14 = rawPart14Curve[i] or lastRawPart14Value
	local Part15 = rawPart15Curve[i] or lastRawPart15Value
	local Part16 = rawPart16Curve[i] or lastRawPart16Value
	local Part17 = rawPart17Curve[i] or lastRawPart17Value
	local Part18 = rawPart18Curve[i] or lastRawPart18Value
	local Part19 = rawPart19Curve[i] or lastRawPart19Value
	local Part20 = rawPart20Curve[i] or lastRawPart20Value
	local Part21 = rawPart21Curve[i] or lastRawPart21Value
	local Part22 = rawPart22Curve[i] or lastRawPart22Value
	local Part23 = rawPart23Curve[i] or lastRawPart23Value
	local Part24 = rawPart24Curve[i] or lastRawPart24Value
	local Part25 = rawPart25Curve[i] or lastRawPart25Value
	local Part26 = rawPart26Curve[i] or lastRawPart26Value
	local Part27 = rawPart27Curve[i] or lastRawPart27Value
	local Part28 = rawPart28Curve[i] or lastRawPart28Value
    local exhaust = rawExhaustCurve[i] or lastRawExhaustValue
    rawCombinedCurve[i] = base * baseMult + intake + exhaust + Part1 + Part2 + Part3 + Part4 + Part5 + Part6 + Part7 + Part8 + Part9 + Part10 + Part11 + Part12 + Part13 + Part14 + Part15 + Part16 + Part17 + Part18 + Part19 + Part20 + Part21 + Part22 + Part23 + Part24 + Part25 + Part26 + Part27 + Part28
  end

  device.maxAvailableRPM = maxAvailableRPM
  device.maxRPM = min(device.maxRPM, maxAvailableRPM)
  device.maxAV = min(device.maxAV, maxAvailableRPM * rpmToAV)

  device.applyRevLimiter = revLimiterDisabledMethod
  device.revLimiterActive = false
  device.revLimiterWasActiveTimer = 999
  device.hasRevLimiter = jbeamData.hasRevLimiter == nil and true or jbeamData.hasRevLimiter --TBD, default should be "no" rev limiter
  if device.hasRevLimiter then
    device.revLimiterType = jbeamData.revLimiterType or "rpmDrop" --alternatives: "timeBased", "soft"
    local revLimiterRPM = jbeamData.revLimiterRPM or device.maxRPM
    device.maxRPM = min(maxAvailableRPM, revLimiterRPM)
    device.maxAV = device.maxRPM * rpmToAV

    if device.revLimiterType == "rpmDrop" then --purely rpm drop based
      device.revLimiterAVDrop = (jbeamData.revLimiterRPMDrop or (jbeamData.maxRPM * 0.03)) * rpmToAV
      device.applyRevLimiter = revLimiterRPMDropMethod
    elseif device.revLimiterType == "timeBased" then --combined both time or rpm drop, whatever happens first
      device.revLimiterCutTime = jbeamData.revLimiterCutTime or 0.15
      device.revLimiterMaxAVDrop = (jbeamData.revLimiterMaxRPMDrop or 500) * rpmToAV
      device.revLimiterActiveTimer = 0
      device.applyRevLimiter = revLimiterTimeMethod
    elseif device.revLimiterType == "soft" then --soft limiter without any "drop", it just smoothly fades out throttle
      device.revLimiterMaxAVOvershoot = (jbeamData.revLimiterSmoothOvershootRPM or 50) * rpmToAV
      device.revLimiterMaxAV = device.maxAV + device.revLimiterMaxAVOvershoot
      device.invRevLimiterRange = 1 / (device.revLimiterMaxAV - device.maxAV)
      device.applyRevLimiter = revLimiterSoftMethod
    else
      log("E", "combustionEngine.init", "Unknown rev limiter type: " .. device.revLimiterType)
      log("E", "combustionEngine.init", "Rev limiter will be disabled!")
      device.hasRevLimiter = false
    end
  end

  device:resetTempRevLimiter()

  --cut off torque below a certain RPM to help stalling
  for i = 0, device.idleRPM * 0.3, 1 do
    rawCombinedCurve[i] = 0
  end

  local combinedTorquePoints = {}
  for i = 0, device.maxRPM, 1 do
    table.insert(combinedTorquePoints, {i, rawCombinedCurve[i] or 0})
  end

  --past redline we want to gracefully reduce the torque for a natural redline
  device.redlineTorqueDropOffRange = clamp(jbeamData.redlineTorqueDropOffRange or 500, 10, device.maxRPM)

  --last usable torque value for a smooth transition to past-maxRPM-drop-off
  local rawMaxRPMTorque = rawCombinedCurve[device.maxRPM] or 0

  --create the drop off past the max rpm for a natural redline
  table.insert(combinedTorquePoints, {device.maxRPM + device.redlineTorqueDropOffRange * 0.5, rawMaxRPMTorque * 0.7})
  table.insert(combinedTorquePoints, {device.maxRPM + device.redlineTorqueDropOffRange, rawMaxRPMTorque / 5})
  table.insert(combinedTorquePoints, {device.maxRPM + device.redlineTorqueDropOffRange * 2, 0})

  --actually create the final torque curve
  device.torqueCurve = createCurve(combinedTorquePoints)

  device.invEngInertia = 1 / device.inertia
  device.halfInvEngInertia = device.invEngInertia * 0.5

  local idleReadErrorRate = jbeamData.idleRPMRoughnessRate or device.idleAVReadErrorRange * 2
  device.idleAVReadErrorSmoother = newTemporalSmoothing(idleReadErrorRate, idleReadErrorRate)
  device.idleAVReadErrorRangeHalf = device.idleAVReadErrorRange * 0.5
  device.maxIdleAV = device.idleAV + device.idleAVReadErrorRangeHalf
  device.minIdleAV = device.idleAV - device.idleAVReadErrorRangeHalf

  local idleAVStartOffsetRate = jbeamData.idleRPMStartRate or 1
  device.idleAVStartOffsetSmoother = newTemporalSmoothingNonLinear(idleAVStartOffsetRate, 100)
  device.idleStartCoef = jbeamData.idleRPMStartCoef or 2

  device.idleTorque = device.torqueCurve[floor(device.idleRPM)] or 0

  --ignition error properties
  --slow
  device.slowIgnitionErrorSmoother = newTemporalSmoothing(2, 2)
  device.slowIgnitionErrorTimer = 0
  device.slowIgnitionErrorChance = 0.0
  device.slowIgnitionErrorInterval = 5
  device.slowIgnitionErrorCoef = 1
  --fast
  device.fastIgnitionErrorSmoother = newTemporalSmoothing(10, 10)
  device.fastIgnitionErrorChance = 0.0
  device.fastIgnitionErrorCoef = 1

  device.brakeSpecificFuelConsumption = 0

  local tempBurnEfficiencyTable = nil
  if not jbeamData.burnEfficiency or type(jbeamData.burnEfficiency) == "number" then
    tempBurnEfficiencyTable = {{0, jbeamData.burnEfficiency or 1}, {1, jbeamData.burnEfficiency or 1}}
  elseif type(jbeamData.burnEfficiency) == "table" then
    tempBurnEfficiencyTable = deepcopy(jbeamData.burnEfficiency)
  end

  local copy = deepcopy(tempBurnEfficiencyTable)
  tempBurnEfficiencyTable = {}
  for k, v in pairs(copy) do
    if type(k) == "number" then
      table.insert(tempBurnEfficiencyTable, {v[1] * 100, v[2]})
    end
  end

  tempBurnEfficiencyTable = createCurve(tempBurnEfficiencyTable)
  device.invBurnEfficiencyTable = {}
  device.invBurnEfficiencyCoef = 1
  for k, v in pairs(tempBurnEfficiencyTable) do
    device.invBurnEfficiencyTable[k] = 1 / v
  end

  device.requiredEnergyType = jbeamData.requiredEnergyType or "gasoline"
  device.energyStorage = jbeamData.energyStorage

  if device.torqueReactionNodes and #device.torqueReactionNodes == 3 and device.torqueReactionNodes[1] >= 0 then
    local pos1 = vec3(v.data.nodes[device.torqueReactionNodes[1]].pos)
    local pos2 = vec3(v.data.nodes[device.torqueReactionNodes[2]].pos)
    local pos3 = vec3(v.data.nodes[device.torqueReactionNodes[3]].pos)
    local avgPos = (((pos1 + pos2) / 2) + pos3) / 2
    device.visualPosition = {x = avgPos.x, y = avgPos.y, z = avgPos.z}
  end

  device.engineNodeID = device.torqueReactionNodes and (device.torqueReactionNodes[1] or v.data.refNodes[0].ref) or v.data.refNodes[0].ref
  if device.engineNodeID < 0 then
    log("W", "combustionEngine.init", "Can't find suitable engine node, using ref node instead!")
    device.engineNodeID = v.data.refNodes[0].ref
  end

  device.engineBlockNodes = {}
  if jbeamData.engineBlock and jbeamData.engineBlock._engineGroup_nodes and #jbeamData.engineBlock._engineGroup_nodes >= 2 then
    device.engineBlockNodes = jbeamData.engineBlock._engineGroup_nodes
  end

  --dump(jbeamData)

  local thermalsFileName = jbeamData.thermalsLuaFileName or "powertrain/combustionEngineThermals"
  device.thermals = require(thermalsFileName)
  device.thermals.init(device, jbeamData)

  if jbeamData.turbocharger and v.data[jbeamData.turbocharger] then
    local turbochargerFileName = jbeamData.turbochargerLuaFileName or "powertrain/turbocharger"
    device.turbocharger = require(turbochargerFileName)
    device.turbocharger.init(device, v.data[jbeamData.turbocharger])
  else
    device.turbocharger = {reset = nop, updateGFX = nop, updateFixedStep = nop, updateSounds = nop, initSounds = nop, resetSounds = nop, getPartCondition = nop, isExisting = false}
  end

  if jbeamData.supercharger and v.data[jbeamData.supercharger] then
    local superchargerFileName = jbeamData.superchargerLuaFileName or "powertrain/supercharger"
    device.supercharger = require(superchargerFileName)
    device.supercharger.init(device, v.data[jbeamData.supercharger])
  else
    device.supercharger = {reset = nop, updateGFX = nop, updateFixedStep = nop, updateSounds = nop, initSounds = nop, resetSounds = nop, getPartCondition = nop, isExisting = false}
  end

  if jbeamData.nitrousOxideInjection and v.data[jbeamData.nitrousOxideInjection] then
    local nitrousOxideFileName = jbeamData.nitrousOxideLuaFileName or "powertrain/nitrousOxideInjection"
    device.nitrousOxideInjection = require(nitrousOxideFileName)
    device.nitrousOxideInjection.init(device, v.data[jbeamData.nitrousOxideInjection])
  else
    device.nitrousOxideInjection = {reset = nop, updateGFX = nop, updateSounds = nop, initSounds = nop, resetSounds = nop, registerStorage = nop, getAddedTorque = nop, getPartCondition = nop, isExisting = false}
  end

  device.torqueData = getTorqueData(device)
  device.maxPower = device.torqueData.maxPower
  device.maxTorque = device.torqueData.maxTorque
  device.maxPowerThrottleMap = device.torqueData.maxPower * psToWatt

  device.breakTriggerBeam = jbeamData.breakTriggerBeam
  if device.breakTriggerBeam and device.breakTriggerBeam == "" then
    --get rid of the break beam if it's just an empty string (cancellation)
    device.breakTriggerBeam = nil
  end

  damageTracker.setDamage("engine", "engineDisabled", false)
  damageTracker.setDamage("engine", "engineLockedUp", false)
  damageTracker.setDamage("engine", "engineReducedTorque", false)
  damageTracker.setDamage("engine", "catastrophicOverrevDamage", false)
  damageTracker.setDamage("engine", "mildOverrevDamage", false)
  damageTracker.setDamage("engine", "catastrophicOverTorqueDamage", false)
  damageTracker.setDamage("engine", "mildOverTorqueDamage", false)
  damageTracker.setDamage("engine", "engineHydrolocked", false)
  damageTracker.setDamage("engine", "engineIsHydrolocking", false)
  damageTracker.setDamage("engine", "impactDamage", false)

  selectUpdates(device)

  return device
end

M.new = new

--local command = "obj:queueGameEngineLua(string.format('scenarios.getScenario().wheelDataCallback(%s)', serialize({wheels.wheels[0].absActive, wheels.wheels[0].angularVelocity, wheels.wheels[0].angularVelocityBrakeCouple}))"

return M
