
TirePressureSystem = {
    name = g_currentModName or "TirePressureSystem",
    path = g_currentModDirectory or "",
    enabled = true,
    minPressure = 0.8,
    maxPressure = 2.8,
    fieldPressure = 1.0,
    roadPressure = 2.4,
    step = 0.2,
    changeRate = 0.22,
    lowPressureMudGrip = 1.45,
    highPressureMudGrip = 0.88,
    lowPressureSinkMul = 0.55,
    highPressureSinkMul = 1.14,
    lowPressureMudBrakeMul = 0.58,
    highPressureMudBrakeMul = 1.18,
    lowPressureRadiusSinkMul = 0.38,
    highPressureRadiusSinkMul = 1.18,
    lowPressureSinkOutMul = 1.28,
    highPressureSinkOutMul = 0.92,
    lowPressureMudSpeedLimitMul = 1.16,
    highPressureMudSpeedLimitMul = 0.94,
    lowPressureMotorLoadMul = 0.72,
    highPressureMotorLoadMul = 1.12,
    shaderRoadW = 0.006,
    shaderFieldW = 0.065,
    physicsFieldW = 0.060,
    physicsRadiusScale = 1.0,
    physicsMinRadiusFactor = 0.94,
    lowPressureDryMul = 0.92,
    highPressureRoadMul = 1.04,
    panelOpen = false,
    _dragSlider = nil,
    _lastSystemInput = {},
    _activeVehicle = nil,
    panelBgAlpha = 0.88,
    panelBgRelPath = "gui/back.dds",
    uiGreenR = 0.48,
    uiGreenG = 0.76,
    uiGreenB = 0.02
}
TirePressureSystem._states = setmetatable({}, {__mode="k"})
TirePressureSystem._wheelStates = setmetatable({}, {__mode="k"})
TirePressureSystem._pressureDisplayStats = setmetatable({}, {__mode="k"})
TirePressureSystem.SAVE_NODE = "mudSystemTirePressure"
TirePressureSystem.SPEC_SAVE_SHORT = "mudsysTirePressureAction"
TirePressureSystem.panelBgPath = TirePressureSystem.path .. TirePressureSystem.panelBgRelPath

local function clamp(v, mn, mx)
    return math.max(mn, math.min(mx, tonumber(v) or mn))
end

local function tirePressureVehicleDeleting(vehicle)
    return vehicle == nil
        or vehicle.__mudSystemDeleting == true
        or vehicle.isDeleting == true
        or vehicle.isDeleted == true
end

local function safeGetShaderParameter(node, name)
    if node == nil or name == nil or getShaderParameter == nil then return nil end
    local ok, x, y, z, w = pcall(getShaderParameter, node, name)
    if ok then return x, y, z, w end
    return nil
end

local function safeSetShaderParameter(node, name, x, y, z, w)
    if node == nil or name == nil or setShaderParameter == nil then return end
    pcall(setShaderParameter, node, name, x or 0, y or 0, z or 0, w or 0, false)
end

local function nodeExistsSafe(node)
    if type(node) ~= "number" or node == 0 then return false end
    if entityExists ~= nil then
        local ok, exists = pcall(entityExists, node)
        return ok and exists == true
    end
    return true
end

local function isShapeNode(node)
    if not nodeExistsSafe(node) then return false end
    if getHasClassId ~= nil and ClassIds ~= nil and ClassIds.SHAPE ~= nil then
        local ok, has = pcall(getHasClassId, node, ClassIds.SHAPE)
        return ok and has == true
    end
    return true
end

local function collectShaderShapes(node, out)
    if not nodeExistsSafe(node) then return end
    if isShapeNode(node) then out[#out+1] = node end
    if getNumOfChildren ~= nil and getChildAt ~= nil then
        local ok, numChildren = pcall(getNumOfChildren, node)
        if ok and numChildren ~= nil then
            for i=0,numChildren-1 do
                local okChild, child = pcall(getChildAt, node, i)
                if okChild then collectShaderShapes(child, out) end
            end
        end
    end
end

local function resolveNodeId(value)
    if value == nil then return nil end
    if type(value) == "number" then return value end
    if type(value) == "table" then
        return resolveNodeId(value.node)
            or resolveNodeId(value.nodeId)
            or resolveNodeId(value.transformId)
            or resolveNodeId(value.shape)
            or resolveNodeId(value.i3dNode)
    end
    return nil
end

local function visualPartNode(visualPart)
    if visualPart == nil then return nil end
    return resolveNodeId(visualPart.node)
        or resolveNodeId(visualPart.rootNode)
        or resolveNodeId(visualPart.visualNode)
        or resolveNodeId(visualPart.transform)
        or resolveNodeId(visualPart.i3dNode)
        or resolveNodeId(visualPart.nodeId)
        or resolveNodeId(visualPart.tireNode)
end

local function consumeSystemInput(key)
    TirePressureSystem._lastSystemInput = TirePressureSystem._lastSystemInput or {}
    local now = g_time or 0
    local last = TirePressureSystem._lastSystemInput[key] or -1000
    if now - last < 140 then
        return false
    end

    TirePressureSystem._lastSystemInput[key] = now
    return true
end

local function stateFor(vehicle)
    if tirePressureVehicleDeleting(vehicle) then return nil end
    local state = TirePressureSystem._states[vehicle]
    if state == nil then
        state = {current=TirePressureSystem.roadPressure, target=TirePressureSystem.roadPressure}
        TirePressureSystem._states[vehicle] = state
    end
    return state
end

function TirePressureSystem:setActiveVehicle(vehicle)
    if not tirePressureVehicleDeleting(vehicle) and vehicle.spec_wheels ~= nil then
        self._activeVehicle = vehicle
    end
end

function TirePressureSystem:getActiveVehicle()
    if not tirePressureVehicleDeleting(self._activeVehicle)
        and self._activeVehicle.spec_wheels ~= nil then
        return self._activeVehicle
    end
    self._activeVehicle = nil

    if g_localPlayer ~= nil and g_localPlayer.getCurrentVehicle ~= nil then
        local vehicle = g_localPlayer:getCurrentVehicle()
        if not tirePressureVehicleDeleting(vehicle) and vehicle.spec_wheels ~= nil then
            self._activeVehicle = vehicle
            return self._activeVehicle
        end
    end

    local mission = g_currentMission
    if mission ~= nil then
        if not tirePressureVehicleDeleting(mission.controlledVehicle)
            and mission.controlledVehicle.spec_wheels ~= nil then
            self._activeVehicle = mission.controlledVehicle
            return self._activeVehicle
        end

        if not tirePressureVehicleDeleting(mission.currentVehicle)
            and mission.currentVehicle.spec_wheels ~= nil then
            self._activeVehicle = mission.currentVehicle
            return self._activeVehicle
        end
    end

    return nil
end

function TirePressureSystem:getControlledVehicle()
    local vehicle = nil

    if g_localPlayer ~= nil and g_localPlayer.getCurrentVehicle ~= nil then
        vehicle = g_localPlayer:getCurrentVehicle()
    end

    local mission = g_currentMission
    if vehicle == nil and mission ~= nil then
        vehicle = mission.controlledVehicle or mission.currentVehicle
    end

    if not tirePressureVehicleDeleting(vehicle) and vehicle.spec_wheels ~= nil then
        self._activeVehicle = vehicle
        return vehicle
    end

    self._activeVehicle = nil
    return nil
end

function TirePressureSystem:getSelectedPressureVehicle(vehicle)
    if vehicle == nil then
        return nil
    end

    local root = vehicle
    if vehicle.getRootVehicle ~= nil then
        local ok, value = pcall(vehicle.getRootVehicle, vehicle)
        if ok and value ~= nil then
            root = value
        end
    elseif vehicle.rootVehicle ~= nil then
        root = vehicle.rootVehicle
    end

    local selected = nil

    -- getIsSelected() is the state actually switched by the game's G action.
    -- Prefer it over getSelectedVehicle(), which may keep the last implement
    -- on some vehicle chains when control returns to the root tractor.
    local candidates = {root}
    if root ~= nil and root.getChildVehicles ~= nil then
        local ok, children = pcall(root.getChildVehicles, root)
        if ok and children ~= nil then
            candidates = children
        end
    elseif root ~= nil and root.childVehicles ~= nil then
        candidates = root.childVehicles
    end

    for _, child in pairs(candidates or {}) do
        if child ~= nil and child.getIsSelected ~= nil then
            local okSelected, isSelected = pcall(
                child.getIsSelected,
                child
            )
            if okSelected and isSelected == true then
                selected = child
                break
            end
        end
    end

    -- Read the root selection object directly as the second source of truth.
    if selected == nil and root ~= nil then
        local currentSelection = root.currentSelection
        local selectionObject = currentSelection ~= nil
            and currentSelection.object or nil
        selected = selectionObject ~= nil and selectionObject.vehicle or nil
    end

    if selected == nil and root ~= nil and root.getSelectedObject ~= nil then
        local ok, object = pcall(root.getSelectedObject, root)
        if ok and object ~= nil then
            selected = object.vehicle
        end
    end

    if selected == nil and root ~= nil and root.getSelectedVehicle ~= nil then
        local ok, value = pcall(root.getSelectedVehicle, root)
        if ok then
            selected = value
        end
    end

    if selected == nil then
        selected = vehicle.spec_wheels ~= nil and vehicle or root
    end

    local wheels = selected ~= nil
        and selected.spec_wheels ~= nil
        and selected.spec_wheels.wheels or nil
    if wheels == nil or next(wheels) == nil then
        return nil
    end

    return selected
end

function TirePressureSystem:ensureSwitchImplementAction(vehicle)
    if vehicle == nil
        or g_inputBinding == nil
        or InputAction == nil
        or InputAction.SWITCH_IMPLEMENT == nil
        or Vehicle == nil
        or Vehicle.actionEventToggleSelection == nil then
        return
    end

    local root = vehicle.rootVehicle or vehicle
    if vehicle ~= root or root.actionEvents == nil then
        return
    end

    local selectableObjects = root.selectableObjects
    if selectableObjects == nil or #selectableObjects <= 1 then
        return
    end

    local action = InputAction.SWITCH_IMPLEMENT
    local registered = root.actionEvents[action]
    local eventId = registered ~= nil and registered.actionEventId or nil
    local eventExists = eventId ~= nil
    if eventExists and g_inputBinding.events ~= nil then
        eventExists = g_inputBinding.events[eventId] ~= nil
    end
    if eventExists then
        return
    end

    local now = g_time or 0
    if now < (root.__tpNextSwitchImplementRepairMs or 0) then
        return
    end
    root.__tpNextSwitchImplementRepairMs = now + 250

    -- registerSelectableObjects() now exposes both the controlled root and
    -- its implements to FS25. Normally the engine creates this G event on its
    -- next action refresh; register the same vanilla callback only if that
    -- event is still missing, for example on an already loaded vehicle chain.
    local success, repairedEventId = root:addActionEvent(
        root.actionEvents,
        action,
        root,
        Vehicle.actionEventToggleSelection,
        false,
        true,
        false,
        true,
        nil,
        nil,
        true
    )

    if success and repairedEventId ~= nil then
        root.__tpSwitchImplementRepairEventId = repairedEventId
        if g_inputBinding.setActionEventTextPriority ~= nil then
            g_inputBinding:setActionEventTextPriority(
                repairedEventId,
                GS_PRIO_LOW
            )
        end
        if root.__tpSwitchImplementRepairLogged ~= true then
            root.__tpSwitchImplementRepairLogged = true
            print(
                "[MudSystemPhysics] Registered SWITCH_IMPLEMENT for the extended vehicle chain"
            )
        end
    end
end

local function wheelKey(wheel)
    return wheel ~= nil and (wheel.physics or wheel) or nil
end

local function wheelStateFor(vehicle, wheel)
    if tirePressureVehicleDeleting(vehicle) then return nil end
    local key = wheelKey(wheel)
    if key == nil then return nil end

    local state = TirePressureSystem._wheelStates[key]
    if state == nil then
        local base = stateFor(vehicle)
        local p = base ~= nil and (base.target or base.current) or TirePressureSystem.roadPressure
        state = {current=p, target=p}
        TirePressureSystem._wheelStates[key] = state
    end

    return state
end

local function xmlHasProperty(xmlFile, key)
    if xmlFile == nil or key == nil then return false end
    if xmlFile.hasProperty ~= nil then
        local ok, has = pcall(xmlFile.hasProperty, xmlFile, key)
        if ok then
            return has == true
        end
    end

    if xmlFile.getValue ~= nil then
        local okTarget, target = pcall(xmlFile.getValue, xmlFile, key .. "#target", nil)
        if okTarget and target ~= nil then
            return true
        end

        local okCurrent, current = pcall(xmlFile.getValue, xmlFile, key .. "#current", nil)
        if okCurrent and current ~= nil then
            return true
        end
    end

    return false
end

function TirePressureSystem:getVehicleSaveNodeKey(key)
    local specKey = self:getVehicleSpecSaveKey(key)
    if specKey == nil then return nil end

    return specKey .. "." .. self.SAVE_NODE
end

function TirePressureSystem:getVehicleSpecSaveKey(key)
    if key == nil then return nil end

    local k = tostring(key)
    local nodeSuffix = "." .. self.SAVE_NODE
    if k:sub(-#nodeSuffix) == nodeSuffix then
        return k
    end

    local modName = g_currentModName or self.name or "FS25_MudSystemPhysics"
    local shortSpec = self.SPEC_SAVE_SHORT or "mudsysTirePressureAction"
    local suffixFull = "." .. tostring(modName) .. "." .. shortSpec
    local suffixShort = "." .. shortSpec
    local doublePrefix = "." .. tostring(modName) .. "." .. tostring(modName) .. "." .. shortSpec

    if k:sub(-#doublePrefix) == doublePrefix then
        return k:sub(1, #k - #doublePrefix) .. suffixFull
    end

    if k:sub(-#suffixFull) == suffixFull then
        return k
    end

    if k:sub(-#suffixShort) == suffixShort then
        return k:sub(1, #k - #suffixShort) .. suffixFull
    end

    return k .. suffixFull
end

function TirePressureSystem:registerSavegameXMLPaths()
    if self._savegameXMLRegistered == true then return end
    if Vehicle == nil or Vehicle.xmlSchemaSavegame == nil or XMLValueType == nil then return end

    self._savegameXMLRegistered = true

    local modName = g_currentModName or self.name or "FS25_MudSystemPhysics"
    local shortSpec = self.SPEC_SAVE_SHORT or "mudsysTirePressureAction"
    local basePath = string.format("vehicles.vehicle(?).%s.%s.%s", tostring(modName), shortSpec, self.SAVE_NODE)
    local schema = Vehicle.xmlSchemaSavegame

    local function registerNode(path)
        schema:register(XMLValueType.FLOAT, path .. "#current", "Mud System tire pressure current average")
        schema:register(XMLValueType.FLOAT, path .. "#target", "Mud System tire pressure target average")
        schema:register(XMLValueType.FLOAT, path .. "#min", "Mud System tire pressure minimum")
        schema:register(XMLValueType.FLOAT, path .. "#max", "Mud System tire pressure maximum")
        schema:register(XMLValueType.INT, path .. ".wheel(?)#index", "Mud System tire pressure wheel index")
        schema:register(XMLValueType.FLOAT, path .. ".wheel(?)#current", "Mud System tire pressure wheel current")
        schema:register(XMLValueType.FLOAT, path .. ".wheel(?)#target", "Mud System tire pressure wheel target")
    end

    registerNode(basePath)
    registerNode(string.format("vehicles.vehicle(?).%s.%s", shortSpec, self.SAVE_NODE))
    registerNode(string.format("vehicles.vehicle(?).%s", self.SAVE_NODE))
    registerNode(string.format("vehicles.vehicle(?).%s.%s.%s.%s", tostring(modName), tostring(modName), shortSpec, self.SAVE_NODE))
end

function TirePressureSystem:getVehicleSaveReadKeys(vehicleKey)
    local keys = {}
    if vehicleKey == nil then return keys end

    local base = tostring(vehicleKey)
    local node = "." .. self.SAVE_NODE
    local shortSpec = self.SPEC_SAVE_SHORT or "mudsysTirePressureAction"
    local modName = g_currentModName or self.name or "FS25_MudSystemPhysics"
    local fullSpec = tostring(modName) .. "." .. shortSpec

    local function add(key)
        if key ~= nil then
            for _, existing in ipairs(keys) do
                if existing == key then return end
            end
            keys[#keys + 1] = key
        end
    end

    if base:sub(-#node) == node then
        add(base)
    else
        add(base .. "." .. fullSpec .. node)
        add(base .. "." .. shortSpec .. node)
        add(base .. node)

        local doubleFull = tostring(modName) .. "." .. fullSpec
        add(base .. "." .. doubleFull .. node)
    end

    return keys
end

function TirePressureSystem:loadVehicleFromSavegame(vehicle, savegame)
    local spec = vehicle ~= nil and vehicle.spec_mudsysTirePressureAction or nil
    if spec == nil then return end

    spec.savedPressure = nil

    if savegame == nil or savegame.xmlFile == nil or savegame.key == nil then
        return
    end

    local xmlFile = savegame.xmlFile
    local saveKey = nil
    for _, key in ipairs(self:getVehicleSaveReadKeys(savegame.key)) do
        if xmlHasProperty(xmlFile, key) then
            saveKey = key
            break
        end
    end

    if saveKey == nil then
        return
    end

    local defaultPressure = self.roadPressure or 2.4
    local data = {
        current = clamp(xmlFile:getValue(saveKey .. "#current", defaultPressure), self.minPressure, self.maxPressure),
        target = clamp(xmlFile:getValue(saveKey .. "#target", defaultPressure), self.minPressure, self.maxPressure),
        wheels = {}
    }

    if xmlFile.iterate ~= nil then
        xmlFile:iterate(saveKey .. ".wheel", function(_, wheelKey)
            local index = xmlFile:getValue(wheelKey .. "#index", nil)
            if index ~= nil then
                local current = xmlFile:getValue(wheelKey .. "#current", data.current)
                local target = xmlFile:getValue(wheelKey .. "#target", data.target)
                data.wheels[index] = {
                    current = clamp(current, self.minPressure, self.maxPressure),
                    target = clamp(target, self.minPressure, self.maxPressure)
                }
            end
        end)
    else
        for i = 1, 64 do
            local wheelKey = string.format("%s.wheel(%d)", saveKey, i - 1)
            local index = xmlFile:getValue(wheelKey .. "#index", nil)
            if index ~= nil then
                data.wheels[index] = {
                    current = clamp(xmlFile:getValue(wheelKey .. "#current", data.current), self.minPressure, self.maxPressure),
                    target = clamp(xmlFile:getValue(wheelKey .. "#target", data.target), self.minPressure, self.maxPressure)
                }
            end
        end
    end

    spec.savedPressure = data
    print(string.format("[MudSystemPhysics] Tire pressure save loaded -> %.2f bar", data.target or data.current or self.roadPressure))
end

function TirePressureSystem:applyVehicleSavegamePressure(vehicle)
    local spec = vehicle ~= nil and vehicle.spec_mudsysTirePressureAction or nil
    local data = spec ~= nil and spec.savedPressure or nil
    if data == nil then return end

    local state = stateFor(vehicle)
    if state ~= nil then
        state.current = data.current or data.target or self.roadPressure
        state.target = data.target or state.current
    end

    local wheels = vehicle ~= nil and vehicle.spec_wheels ~= nil and vehicle.spec_wheels.wheels or nil
    for wheelIndex, wheel in pairs(wheels or {}) do
        local ws = wheelStateFor(vehicle, wheel)
        if ws ~= nil then
            local wheelData = data.wheels[wheelIndex] or data.wheels[tonumber(wheelIndex)]
            ws.current = (wheelData ~= nil and wheelData.current) or data.current or data.target or self.roadPressure
            ws.target = (wheelData ~= nil and wheelData.target) or data.target or ws.current
        end
    end

    spec.savedPressure = nil
    self:updateVehicle(vehicle, 0)
    print(string.format("[MudSystemPhysics] Tire pressure save applied -> %.2f bar", state ~= nil and (state.target or state.current or self.roadPressure) or self.roadPressure))
end

function TirePressureSystem:ensureVehicleSavegamePressureApplied(vehicle)
    local spec = vehicle ~= nil and vehicle.spec_mudsysTirePressureAction or nil
    if spec ~= nil and spec.savedPressure ~= nil then
        self:applyVehicleSavegamePressure(vehicle)
    end
end

function TirePressureSystem:saveVehicleToXMLFile(vehicle, xmlFile, key)
    if vehicle == nil or xmlFile == nil or key == nil then return end

    local wheels = vehicle.spec_wheels ~= nil and vehicle.spec_wheels.wheels or nil
    if wheels == nil then return end

    local current, target, wheelCount = self:getVehicleWheelPressureAverage(vehicle)
    if wheelCount == nil or wheelCount <= 0 then
        return
    end

    local saveKey = self:getVehicleSaveNodeKey(key)
    if saveKey == nil then return end

    xmlFile:setValue(saveKey .. "#current", current or self.roadPressure)
    xmlFile:setValue(saveKey .. "#target", target or current or self.roadPressure)
    xmlFile:setValue(saveKey .. "#min", self.minPressure or 0.8)
    xmlFile:setValue(saveKey .. "#max", self.maxPressure or 2.8)

    local outIndex = 0
    for wheelIndex, wheel in pairs(wheels or {}) do
        local ws = wheelStateFor(vehicle, wheel)
        if ws ~= nil then
            local wheelKey = string.format("%s.wheel(%d)", saveKey, outIndex)
            xmlFile:setValue(wheelKey .. "#index", wheelIndex)
            xmlFile:setValue(wheelKey .. "#current", ws.current or ws.target or current or self.roadPressure)
            xmlFile:setValue(wheelKey .. "#target", ws.target or ws.current or target or self.roadPressure)
            outIndex = outIndex + 1
        end
    end
end

function TirePressureSystem:getVehicleWheelPressureAverage(vehicle)
    self:ensureVehicleSavegamePressureApplied(vehicle)

    local wheels = vehicle ~= nil and vehicle.spec_wheels ~= nil and vehicle.spec_wheels.wheels or nil
    local sum, targetSum, count = 0, 0, 0

    for _, wheel in pairs(wheels or {}) do
        local ws = wheelStateFor(vehicle, wheel)
        if ws ~= nil then
            local current = ws.current or ws.target or self.roadPressure
            local target = ws.target or current
            sum = sum + current
            targetSum = targetSum + target
            count = count + 1
        end
    end

    if count > 0 then
        return sum / count, targetSum / count, count
    end

    local state = stateFor(vehicle)
    if state ~= nil then
        return state.current or self.roadPressure, state.target or state.current or self.roadPressure, 0
    end

    return self.roadPressure, self.roadPressure, 0
end

function TirePressureSystem:getCombinationPressureAverage(vehicle)
    local sum, targetSum, count = 0, 0, 0

    for _, child in ipairs(self:getCombination(vehicle)) do
        local current, target, wheelCount = self:getVehicleWheelPressureAverage(child)
        if wheelCount ~= nil and wheelCount > 0 then
            sum = sum + current * wheelCount
            targetSum = targetSum + target * wheelCount
            count = count + wheelCount
        end
    end

    if count > 0 then
        return sum / count, targetSum / count, count
    end

    return self:getVehicleWheelPressureAverage(vehicle)
end

function TirePressureSystem:getWheelPressure01(wheelPhysics)
    local vehicle = wheelPhysics ~= nil and wheelPhysics.vehicle or nil
    self:ensureVehicleSavegamePressureApplied(vehicle)

    local wheel = wheelPhysics ~= nil and wheelPhysics.wheel or nil
    local state = wheelStateFor(vehicle, wheel)
    if state == nil then return self:getPressure01(vehicle) end
    return clamp((state.current-self.minPressure)/math.max(0.01,self.maxPressure-self.minPressure),0,1)
end

function TirePressureSystem:getPressure01(vehicle)
    local current = self:getVehicleWheelPressureAverage(vehicle)
    return clamp((current-self.minPressure)/math.max(0.01,self.maxPressure-self.minPressure),0,1)
end

function TirePressureSystem:getSinkMultiplier(wheelPhysics)
    if not self.enabled then return 1 end
    local p = self:getWheelPressure01(wheelPhysics)
    return (self.lowPressureSinkMul or 0.55) + ((self.highPressureSinkMul or 1.14) - (self.lowPressureSinkMul or 0.55)) * p
end

function TirePressureSystem:getVehicleSinkMultiplier(vehicle)
    if not self.enabled then return 1 end
    local p = self:getPressure01(vehicle)
    return (self.lowPressureSinkMul or 0.55) + ((self.highPressureSinkMul or 1.14) - (self.lowPressureSinkMul or 0.55)) * p
end

function TirePressureSystem:getMudGripMultiplier(wheelPhysics)
    if not self.enabled then return 1 end
    local p = self:getWheelPressure01(wheelPhysics)
    return (self.lowPressureMudGrip or 1.45) + ((self.highPressureMudGrip or 0.88) - (self.lowPressureMudGrip or 1.45)) * p
end

function TirePressureSystem:getDryFrictionMultiplier(wheelPhysics)
    if not self.enabled then return 1 end
    local p = self:getWheelPressure01(wheelPhysics)
    return (self.lowPressureDryMul or 0.92) + ((self.highPressureRoadMul or 1.04) - (self.lowPressureDryMul or 0.92)) * p
end

function TirePressureSystem:getMudBrakeMultiplier(wheelPhysicsOrVehicle)
    if not self.enabled then return 1 end
    local p
    if wheelPhysicsOrVehicle ~= nil and wheelPhysicsOrVehicle.spec_wheels ~= nil then
        p = self:getPressure01(wheelPhysicsOrVehicle)
    else
        p = self:getWheelPressure01(wheelPhysicsOrVehicle)
    end
    return (self.lowPressureMudBrakeMul or 0.58) + ((self.highPressureMudBrakeMul or 1.18) - (self.lowPressureMudBrakeMul or 0.58)) * p
end

function TirePressureSystem:getSinkOutMultiplier(wheelPhysicsOrVehicle)
    if not self.enabled then return 1 end
    local p
    if wheelPhysicsOrVehicle ~= nil and wheelPhysicsOrVehicle.spec_wheels ~= nil then
        p = self:getPressure01(wheelPhysicsOrVehicle)
    else
        p = self:getWheelPressure01(wheelPhysicsOrVehicle)
    end
    return (self.lowPressureSinkOutMul or 1.28) + ((self.highPressureSinkOutMul or 0.92) - (self.lowPressureSinkOutMul or 1.28)) * p
end

function TirePressureSystem:getRadiusSinkMultiplier(wheelPhysicsOrVehicle)
    if not self.enabled then return 1 end
    local p
    if wheelPhysicsOrVehicle ~= nil and wheelPhysicsOrVehicle.spec_wheels ~= nil then
        p = self:getPressure01(wheelPhysicsOrVehicle)
    else
        p = self:getWheelPressure01(wheelPhysicsOrVehicle)
    end
    return (self.lowPressureRadiusSinkMul or 0.38) + ((self.highPressureRadiusSinkMul or 1.18) - (self.lowPressureRadiusSinkMul or 0.38)) * p
end

function TirePressureSystem:getMudSpeedLimitMultiplier(vehicle)
    if not self.enabled then return 1 end
    local p = self:getPressure01(vehicle)
    return (self.lowPressureMudSpeedLimitMul or 1.16) + ((self.highPressureMudSpeedLimitMul or 0.94) - (self.lowPressureMudSpeedLimitMul or 1.16)) * p
end

function TirePressureSystem:getMotorLoadMultiplier(wheelPhysicsOrVehicle)
    if not self.enabled then return 1 end
    local p
    if wheelPhysicsOrVehicle ~= nil and wheelPhysicsOrVehicle.spec_wheels ~= nil then
        p = self:getPressure01(wheelPhysicsOrVehicle)
    else
        p = self:getWheelPressure01(wheelPhysicsOrVehicle)
    end
    return (self.lowPressureMotorLoadMul or 0.72) + ((self.highPressureMotorLoadMul or 1.12) - (self.lowPressureMotorLoadMul or 0.72)) * p
end

function TirePressureSystem:getShaderWFromPressure01(pressure01)
    local p = clamp(pressure01, 0, 1)
    local roadW = self.shaderRoadW or 0.008
    local fieldW = self.shaderFieldW or 0.030
    return fieldW + (roadW - fieldW) * p
end

function TirePressureSystem:getRadiusFromPressure01(originalRadius, pressure01)
    if originalRadius == nil then
        return nil
    end

    local p = clamp(pressure01, 0, 1)
    local roadW = self.shaderRoadW or 0.008
    local fieldW = self.physicsFieldW or 0.030
    local newW = fieldW + (roadW - fieldW) * p
    local deltaW = math.max(0, newW - roadW) * (self.physicsRadiusScale or 1.0)
    local minRadius = originalRadius * (self.physicsMinRadiusFactor or 0.94)

    return math.min(originalRadius, math.max(originalRadius - deltaW, minRadius))
end

function TirePressureSystem:setWheelTargetLocal(vehicle, wheelIndex, pressure)
    local wheels = vehicle ~= nil and vehicle.spec_wheels ~= nil and vehicle.spec_wheels.wheels or nil
    local wheel = wheels ~= nil and wheels[wheelIndex or 0] or nil
    local state = wheelStateFor(vehicle, wheel)
    if state ~= nil then
        if PunctureSystem ~= nil and PunctureSystem.isWheelPunctured ~= nil
            and PunctureSystem:isWheelPunctured(vehicle, wheelIndex) then
            pressure = self.minPressure
        end
        local old = state.target or state.current or self.roadPressure
        state.target = clamp(pressure,self.minPressure,self.maxPressure)
        if math.abs(state.target - old) >= 0.05 then
            print(string.format("[MudSystemPhysics] Tire pressure wheel %s target -> %.2f bar", tostring(wheelIndex), state.target))
        end

        local avgCurrent, avgTarget = self:getVehicleWheelPressureAverage(vehicle)
        local vehicleState = stateFor(vehicle)
        if vehicleState ~= nil then
            vehicleState.current = avgCurrent
            vehicleState.target = avgTarget
        end
    end
end

function TirePressureSystem:setVehicleTargetLocal(vehicle, pressure, wheelIndex)
    pressure = clamp(pressure,self.minPressure,self.maxPressure)

    if wheelIndex ~= nil and wheelIndex > 0 then
        self:setWheelTargetLocal(vehicle, wheelIndex, pressure)
        return
    end

    local state = stateFor(vehicle)
    if state ~= nil then
        local old = state.target or state.current or self.roadPressure
        state.target = pressure
        if math.abs(state.target - old) >= 0.05 then
            print(string.format("[MudSystemPhysics] Tire pressure vehicle target -> %.2f bar", state.target))
        end
    end

    local wheels = vehicle ~= nil and vehicle.spec_wheels ~= nil and vehicle.spec_wheels.wheels or nil
    for index, wheel in pairs(wheels or {}) do
        local ws = wheelStateFor(vehicle, wheel)
        if ws ~= nil then
            if PunctureSystem ~= nil and PunctureSystem.isWheelPunctured ~= nil
                and PunctureSystem:isWheelPunctured(vehicle, index) then
                ws.target = self.minPressure
            else
                ws.target = pressure
            end
        end
    end

    if state ~= nil then
        local _, averageTarget = self:getVehicleWheelPressureAverage(vehicle)
        state.target = averageTarget
    end
end

function TirePressureSystem:buildWheelVisualCache(wheel, force)
    if wheel == nil then return end
    if force ~= true and wheel.__tpVisualCacheBuilt == true
        and wheel.__tpShaderShapes ~= nil
        and #wheel.__tpShaderShapes > 0 then
        return
    end

    wheel.__tpVisualCacheBuilt = true
    wheel.__tpShaderShapes = {}

    local seenParts = {}

    local function addVisualPart(part)
        if part == nil or seenParts[part] then return end

        if WheelVisualPartTire ~= nil and part.isa ~= nil and not part:isa(WheelVisualPartTire) then
            return
        end

        seenParts[part] = true

        local node = visualPartNode(part)
        if node == nil then return end

        local shapes = {}
        collectShaderShapes(node, shapes)
        for _, shape in ipairs(shapes) do
            local x, y, z, w = safeGetShaderParameter(shape, "morphPos")
            if x ~= nil then
                wheel.__tpShaderShapes[#wheel.__tpShaderShapes+1] = {
                    node = shape,
                    x = x,
                    y = y,
                    z = z,
                    w = w
                }
            end
        end
    end

    local function scanVisualWheel(vw)
        if vw ~= nil and vw.visualParts ~= nil then
            for _, part in ipairs(vw.visualParts) do addVisualPart(part) end
        end
    end

    local function scanWheel(w)
        if w == nil then return end
        scanVisualWheel(w.visualWheel)
        if w.visualWheels ~= nil then
            for _, vw in ipairs(w.visualWheels) do scanVisualWheel(vw) end
        end
        if w.visualParts ~= nil then
            for _, part in ipairs(w.visualParts) do addVisualPart(part) end
        end
        if w.additionalWheels ~= nil then
            for _, aw in ipairs(w.additionalWheels) do scanWheel(aw) end
        end
    end

    scanWheel(wheel)

    if #wheel.__tpShaderShapes > 0 and self._visualLogged ~= true then
        self._visualLogged = true
        print(string.format("[MudSystemPhysics] Tire pressure visual shader cache active (%d shapes)", #wheel.__tpShaderShapes))
    elseif #wheel.__tpShaderShapes == 0 and wheel.__tpVisualEmptyLogged ~= true then
        wheel.__tpVisualEmptyLogged = true
        print("[MudSystemPhysics] Tire pressure visual cache empty; will retry after wheel visuals load")
    end
end

function TirePressureSystem:prepareVehicle(vehicle)
    if tirePressureVehicleDeleting(vehicle) or vehicle.spec_wheels == nil then
        return
    end

    for _, wheel in pairs(vehicle.spec_wheels.wheels or {}) do
        wheelStateFor(vehicle, wheel)
        self:buildWheelVisualCache(wheel, true)
    end

    self:ensureVehicleSavegamePressureApplied(vehicle)
end

function TirePressureSystem:clearVehicle(vehicle, deleting)
    if vehicle == nil then return end
    deleting = deleting == true or tirePressureVehicleDeleting(vehicle)
    local wheels = vehicle ~= nil and vehicle.spec_wheels ~= nil and vehicle.spec_wheels.wheels or nil
    for wheelIndex, wheel in pairs(wheels or {}) do
        if MudSystemWorldSounds ~= nil then
            MudSystemWorldSounds:stopLoop(vehicle, "pressure:" .. tostring(wheelIndex))
        end
        local wheelKeyValue = wheelKey(wheel)
        if deleting and wheelKeyValue ~= nil then
            self._wheelStates[wheelKeyValue] = nil
        end
        local wp = wheel.physics
        if wp ~= nil then
            wp.__tpDesiredRadius = nil
            if not deleting and _G.__MudRadiusCombiner ~= nil then
                _G.__MudRadiusCombiner.apply(wp, 0.001)
            end
        end
        if deleting then
            wheel.__tpShaderShapes = nil
            wheel.__tpVisualRetryTimer = nil
        end
    end

    if deleting then
        self._states[vehicle] = nil
        self._pressureDisplayStats[vehicle] = nil
        if self._activeVehicle == vehicle then
            self._activeVehicle = nil
        end
        if self._dragSlider ~= nil and self._dragSlider.entry ~= nil
            and self._dragSlider.entry.vehicle == vehicle then
            self._dragSlider = nil
        end
    end
end

function TirePressureSystem:updateWheelPressureSound(vehicle, wheelIndex, wheel, state)
    if MudSystemWorldSounds == nil or vehicle == nil then return end
    local tag = "pressure:" .. tostring(wheelIndex)
    local changing = self.enabled == true and state ~= nil
        and math.abs((state.target or state.current)-(state.current or 0)) > 0.002
    if changing then
        local node = wheel ~= nil and (wheel.repr or wheel.driveNode) or vehicle.rootNode
        MudSystemWorldSounds:startLoop(vehicle, tag, "pressure", node, nil)
    else
        MudSystemWorldSounds:stopLoop(vehicle, tag)
    end
end

function TirePressureSystem:applyWheelVisualPressure(wheel, pressure01)
    if wheel == nil then return end
    self:buildWheelVisualCache(wheel)

    if wheel.__tpShaderShapes == nil or #wheel.__tpShaderShapes == 0 then
        wheel.__tpVisualRetryTimer = (wheel.__tpVisualRetryTimer or 0) + 1
        if wheel.__tpVisualRetryTimer > 30 then
            wheel.__tpVisualRetryTimer = 0
            self:buildWheelVisualCache(wheel, true)
        end
    end

    if wheel.__tpShaderShapes ~= nil and #wheel.__tpShaderShapes > 0 then
        local newW = self:getShaderWFromPressure01(pressure01)

        for _, shape in ipairs(wheel.__tpShaderShapes) do
            safeSetShaderParameter(shape.node, "morphPos", shape.x, shape.y, shape.z, newW)
            safeSetShaderParameter(shape.node, "prevMorphPos", shape.x, shape.y, shape.z, newW)
        end
    end
end

function TirePressureSystem:getCombination(vehicle)
    local root = vehicle
    if root ~= nil and root.getRootVehicle ~= nil then root = root:getRootVehicle() or root end
    local list = root ~= nil and root.getChildVehicles ~= nil and root:getChildVehicles() or {root}
    local result, seen = {}, {}

    local function add(child)
        if child ~= nil and not seen[child] then
            seen[child] = true
            result[#result+1] = child
        end
    end

    -- Keep the root first and the attachment order stable. Apart from making
    -- the panel predictable this also prevents cards from swapping places
    -- between frames because pairs() does not guarantee array order.
    add(root)
    for _, child in ipairs(list or {}) do
        add(child)
    end
    for _, child in pairs(list or {}) do
        add(child)
    end

    return result
end

function TirePressureSystem:setCombinationTarget(vehicle, pressure)
    for _, child in ipairs(self:getCombination(vehicle)) do TirePressureEvent.send(child,pressure) end
end

function TirePressureSystem:adjustCombinationTarget(vehicle, delta)
    for _, child in ipairs(self:getCombination(vehicle)) do
        local wheels = child ~= nil and child.spec_wheels ~= nil and child.spec_wheels.wheels or nil
        local changed = false
        for wheelIndex, wheel in pairs(wheels or {}) do
            local ws = wheelStateFor(child, wheel)
            if ws ~= nil then
                TirePressureEvent.send(child, (ws.target or self.roadPressure) + delta, wheelIndex)
                changed = true
            end
        end

        if not changed then
            local state = stateFor(child)
            TirePressureEvent.send(child, (state ~= nil and state.target or self.roadPressure) + delta)
        end
    end
end

function TirePressureSystem:onPressureDown(vehicle)
    if not consumeSystemInput("down") then return end
    vehicle = vehicle or (g_currentMission and g_currentMission.controlledVehicle)
    if self.enabled and vehicle ~= nil then
        local selectedVehicle = self:getSelectedPressureVehicle(vehicle)
        if selectedVehicle == nil then
            print("[MudSystemPhysics] Tire pressure input ignored: selected component has no wheels")
            return
        end

        self:setActiveVehicle(selectedVehicle)
        print(string.format(
            "[MudSystemPhysics] Tire pressure input: automatic FIELD target %.2f bar",
            self.fieldPressure or 1.0
        ))
        TirePressureEvent.send(selectedVehicle, self.fieldPressure)
    end
end

function TirePressureSystem:onPressureUp(vehicle)
    if not consumeSystemInput("up") then return end
    vehicle = vehicle or (g_currentMission and g_currentMission.controlledVehicle)
    if self.enabled and vehicle ~= nil then
        local selectedVehicle = self:getSelectedPressureVehicle(vehicle)
        if selectedVehicle == nil then
            print("[MudSystemPhysics] Tire pressure input ignored: selected component has no wheels")
            return
        end

        self:setActiveVehicle(selectedVehicle)
        print(string.format(
            "[MudSystemPhysics] Tire pressure input: automatic ROAD target %.2f bar",
            self.roadPressure or 2.4
        ))
        TirePressureEvent.send(selectedVehicle, self.roadPressure)
    end
end

function TirePressureSystem:onPressurePreset(vehicle)
    vehicle = vehicle or (g_currentMission and g_currentMission.controlledVehicle)
    if self.enabled and vehicle ~= nil then
        self:setActiveVehicle(vehicle)
        local state = stateFor(vehicle)
        local middle = (self.fieldPressure+self.roadPressure)*0.5
        self:setCombinationTarget(vehicle,state.target>middle and self.fieldPressure or self.roadPressure)
    end
end

function TirePressureSystem:onTogglePanel(vehicle)
    if not consumeSystemInput("panel") then return end
    vehicle = vehicle or (g_currentMission and g_currentMission.controlledVehicle)
    if not self.enabled or vehicle == nil then return end
    self:setActiveVehicle(vehicle)
    self:setPanelOpen(not self.panelOpen)
    print(string.format("[MudSystemPhysics] Tire pressure panel %s", self.panelOpen and "opened" or "closed"))
end

function TirePressureSystem:updatePanelCameraInputBlock()
    if self.panelOpen ~= true
        or g_inputBinding == nil
        or InputAction == nil then
        return
    end

    local contextName = Vehicle ~= nil and Vehicle.INPUT_CONTEXT_NAME or "VEHICLE"
    local context = g_inputBinding.contexts ~= nil and g_inputBinding.contexts[contextName] or nil
    if context == nil or context.actionEvents == nil or g_inputBinding.nameActions == nil then
        return
    end

    self._panelCameraEventStates = self._panelCameraEventStates or {}
    local actions = {
        InputAction.AXIS_LOOK_UPDOWN_VEHICLE,
        InputAction.AXIS_LOOK_LEFTRIGHT_VEHICLE
    }

    for _, actionName in ipairs(actions) do
        local action = actionName ~= nil and g_inputBinding.nameActions[actionName] or nil
        local events = action ~= nil and context.actionEvents[action] or nil
        for _, event in ipairs(events or {}) do
            local eventId = event.id
            if eventId ~= nil and self._panelCameraEventStates[event] == nil then
                self._panelCameraEventStates[event] = {
                    eventId = eventId,
                    wasActive = event.isActive == true
                }
            end

            if eventId ~= nil and event.isActive == true
                and g_inputBinding.setActionEventActive ~= nil then
                g_inputBinding:setActionEventActive(eventId, false)
            end
        end
    end

    -- Remove any movement accumulated in the same frame in which the panel
    -- was opened. Subsequent mouse movement is stopped by the inactive events.
    local camera = g_activeVehicleCamera
    if camera ~= nil and camera.lastInputValues ~= nil then
        camera.lastInputValues.upDown = 0
        camera.lastInputValues.leftRight = 0
    end
end

function TirePressureSystem:restorePanelCameraInput()
    local savedStates = self._panelCameraEventStates
    self._panelCameraEventStates = nil

    if savedStates == nil
        or g_inputBinding == nil
        or g_inputBinding.events == nil
        or g_inputBinding.setActionEventActive == nil then
        return
    end

    for event, saved in pairs(savedStates) do
        -- Camera changes can replace action events while the panel is open.
        -- Restore only the exact surviving event, never a reused numeric id.
        if g_inputBinding.events[saved.eventId] == event then
            g_inputBinding:setActionEventActive(saved.eventId, saved.wasActive == true)
        end
    end
end

function TirePressureSystem:setPanelOpen(open)
    local wasOpen = self.panelOpen == true
    local willOpen = open == true

    if wasOpen == willOpen then
        return
    end

    self.panelOpen = willOpen
    self._dragSlider = nil
    if willOpen then
        self._panelPage = 1
        self:updatePanelCameraInputBlock()
    else
        self:restorePanelCameraInput()
    end

    if g_inputBinding ~= nil and g_inputBinding.setShowMouseCursor ~= nil then
        if willOpen then
            if g_inputBinding.getShowMouseCursor ~= nil then
                self._mouseCursorStateBeforePanel = g_inputBinding:getShowMouseCursor() == true
            else
                self._mouseCursorStateBeforePanel = false
            end
            g_inputBinding:setShowMouseCursor(true, true)
        else
            g_inputBinding:setShowMouseCursor(self._mouseCursorStateBeforePanel == true, true)
            self._mouseCursorStateBeforePanel = nil
        end
    end
end

function TirePressureSystem:getVehiclePanelName(vehicle, fallbackIndex)
    if vehicle ~= nil and type(vehicle.__mudsysPressurePanelName) == "string" then
        return vehicle.__mudsysPressurePanelName
    end

    local name = nil

    if vehicle ~= nil and vehicle.getFullName ~= nil then
        local ok, value = pcall(vehicle.getFullName, vehicle)
        if ok and type(value) == "string" and value ~= "" then
            name = value
        end
    end

    if name == nil and vehicle ~= nil and vehicle.getName ~= nil then
        local ok, value = pcall(vehicle.getName, vehicle)
        if ok and type(value) == "string" and value ~= "" then
            name = value
        end
    end

    if name == nil and vehicle ~= nil and type(vehicle.typeDesc) == "string" then
        name = vehicle.typeDesc
    end

    name = name or string.format("Vehicle %d", fallbackIndex or 1)
    if vehicle ~= nil then
        vehicle.__mudsysPressurePanelName = name
    end
    return name
end

function TirePressureSystem:getWheelPanelPosition(vehicle, wheel)
    if vehicle == nil or wheel == nil or localToLocal == nil then
        return nil, nil
    end

    if wheel.__mudsysPanelPositionVehicle == vehicle
        and wheel.__mudsysPanelLocalX ~= nil
        and wheel.__mudsysPanelLocalZ ~= nil then
        return wheel.__mudsysPanelLocalX, wheel.__mudsysPanelLocalZ
    end

    local referenceNode = vehicle.directionReferenceNode
    if referenceNode == nil and vehicle.components ~= nil and vehicle.components[1] ~= nil then
        referenceNode = vehicle.components[1].node
    end

    local wheelNode = wheel.repr or wheel.driveNode
    if not nodeExistsSafe(referenceNode) or not nodeExistsSafe(wheelNode) then
        return nil, nil
    end

    local ok, x, _, z = pcall(localToLocal, wheelNode, referenceNode, 0, 0, 0)
    if ok then
        wheel.__mudsysPanelPositionVehicle = vehicle
        wheel.__mudsysPanelLocalX = x
        wheel.__mudsysPanelLocalZ = z
        return x, z
    end

    return nil, nil
end

function TirePressureSystem:labelWheelEntries(vehicle, entries)
    local zMin, zMax = math.huge, -math.huge
    local positioned = 0

    for _, entry in ipairs(entries or {}) do
        if entry.panelZ ~= nil then
            zMin = math.min(zMin, entry.panelZ)
            zMax = math.max(zMax, entry.panelZ)
            positioned = positioned + 1
        end
    end

    -- A drivable machine has a meaningful front/rear axle split. Wheels on
    -- trailers and implements normally sit behind their attachment point, so
    -- keeping them in one rear group produces the useful "Rear 1..N" view.
    local isDrivable = vehicle ~= nil
        and (vehicle.spec_enterable ~= nil or vehicle.spec_motorized ~= nil)
    local hasSeparatedAxles = isDrivable and positioned >= 2 and zMax-zMin > 0.12
    local middleZ = hasSeparatedAxles and (zMin+zMax)*0.5 or nil

    for _, entry in ipairs(entries or {}) do
        -- GIANTS vehicles face local +Z. A single physical axle and all
        -- non-drivable attached equipment are treated as rear.
        entry.panelGroup = middleZ ~= nil
            and entry.panelZ ~= nil
            and entry.panelZ >= middleZ
            and "front"
            or "rear"
    end

    table.sort(entries, function(a, b)
        local aRank = a.panelGroup == "front" and 1 or 2
        local bRank = b.panelGroup == "front" and 1 or 2
        if aRank ~= bRank then
            return aRank < bRank
        end

        if a.panelZ ~= nil and b.panelZ ~= nil and math.abs(a.panelZ-b.panelZ) > 0.04 then
            return a.panelZ > b.panelZ
        end

        if a.isLeft ~= b.isLeft then
            return a.isLeft == true
        end

        if a.panelX ~= nil and b.panelX ~= nil and math.abs(a.panelX-b.panelX) > 0.01 then
            return a.panelX > b.panelX
        end

        local aIndex = tonumber(a.wheelIndex)
        local bIndex = tonumber(b.wheelIndex)
        if aIndex ~= nil and bIndex ~= nil then
            return aIndex < bIndex
        end
        return tostring(a.wheelIndex) < tostring(b.wheelIndex)
    end)

    local frontText = "Front"
    local rearText = "Rear"
    if g_i18n ~= nil and g_i18n.getText ~= nil then
        frontText = g_i18n:getText("mudsys_tp_front") or frontText
        rearText = g_i18n:getText("mudsys_tp_rear") or rearText
    end

    local counters = {front=0, rear=0}
    for _, entry in ipairs(entries or {}) do
        local group = entry.panelGroup
        counters[group] = counters[group] + 1
        entry.label = string.format(
            "%s %d",
            group == "front" and frontText or rearText,
            counters[group]
        )
    end
end

function TirePressureSystem:getWheelGroups(vehicle)
    local groups = {}

    for componentIndex, child in ipairs(self:getCombination(vehicle)) do
        local entries = {}
        local wheels = child ~= nil and child.spec_wheels ~= nil and child.spec_wheels.wheels or nil

        local seenWheels = {}
        local function addWheel(wheelIndex, wheel)
            if wheel == nil or seenWheels[wheel] then
                return
            end

            seenWheels[wheel] = true
            local ws = wheelStateFor(child, wheel)
            local panelX, panelZ = self:getWheelPanelPosition(child, wheel)
            entries[#entries+1] = {
                vehicle = child,
                wheel = wheel,
                wheelIndex = wheelIndex,
                state = ws,
                panelX = panelX,
                panelZ = panelZ,
                isLeft = wheel.isLeft == true
            }
        end

        for wheelIndex, wheel in ipairs(wheels or {}) do
            addWheel(wheelIndex, wheel)
        end
        for wheelIndex, wheel in pairs(wheels or {}) do
            addWheel(wheelIndex, wheel)
        end

        if #entries > 0 then
            self:labelWheelEntries(child, entries)
            groups[#groups+1] = {
                vehicle = child,
                name = self:getVehiclePanelName(child, componentIndex),
                entries = entries
            }
        end
    end

    return groups
end

function TirePressureSystem:getPressureDisplayStats(vehicle)
    if vehicle == nil then return nil end

    local now = tonumber(g_time)
    local cached = self._pressureDisplayStats[vehicle]
    if now ~= nil and cached ~= nil and cached.time == now then
        return cached.values
    end

    local function collectVehicle(sourceVehicle)
        local result = {
            currentSum=0,
            targetSum=0,
            count=0,
            frontCurrentSum=0,
            frontTargetSum=0,
            frontCount=0,
            rearCurrentSum=0,
            rearTargetSum=0,
            rearCount=0
        }
        if sourceVehicle == nil or sourceVehicle.spec_wheels == nil then return result end

        self:ensureVehicleSavegamePressureApplied(sourceVehicle)
        local entries, seen = {}, {}
        for wheelIndex, wheel in pairs(sourceVehicle.spec_wheels.wheels or {}) do
            if wheel ~= nil and not seen[wheel] then
                seen[wheel] = true
                local panelX, panelZ = self:getWheelPanelPosition(sourceVehicle, wheel)
                entries[#entries+1] = {
                    wheel=wheel,
                    wheelIndex=wheelIndex,
                    panelX=panelX,
                    panelZ=panelZ,
                    isLeft=wheel.isLeft == true
                }
            end
        end
        self:labelWheelEntries(sourceVehicle, entries)

        for _, entry in ipairs(entries) do
            local ws = wheelStateFor(sourceVehicle, entry.wheel)
            local current = ws ~= nil
                and (ws.current or ws.target) or self.roadPressure
            local target = ws ~= nil
                and (ws.target or ws.current) or current
            local group = entry.panelGroup == "front" and "front" or "rear"

            result.currentSum = result.currentSum + current
            result.targetSum = result.targetSum + target
            result.count = result.count + 1
            if group == "front" then
                result.frontCurrentSum = result.frontCurrentSum + current
                result.frontTargetSum = result.frontTargetSum + target
                result.frontCount = result.frontCount + 1
            else
                result.rearCurrentSum = result.rearCurrentSum + current
                result.rearTargetSum = result.rearTargetSum + target
                result.rearCount = result.rearCount + 1
            end
        end

        if result.count == 0 then
            local current, target = self:getVehicleWheelPressureAverage(sourceVehicle)
            result.currentSum = current or self.roadPressure
            result.targetSum = target or result.currentSum
            result.count = 1
        end
        return result
    end

    local localStats = collectVehicle(vehicle)
    local localCurrent = localStats.currentSum / math.max(1, localStats.count)
    local localTarget = localStats.targetSum / math.max(1, localStats.count)
    local frontPressure = localStats.frontCount > 0
        and localStats.frontCurrentSum/localStats.frontCount or localCurrent
    local frontTarget = localStats.frontCount > 0
        and localStats.frontTargetSum/localStats.frontCount or localTarget
    local rearPressure = localStats.rearCount > 0
        and localStats.rearCurrentSum/localStats.rearCount or localCurrent
    local rearTarget = localStats.rearCount > 0
        and localStats.rearTargetSum/localStats.rearCount or localTarget

    local root = vehicle
    if vehicle.getRootVehicle ~= nil then
        local ok, value = pcall(vehicle.getRootVehicle, vehicle)
        if ok and value ~= nil then root = value end
    elseif vehicle.rootVehicle ~= nil then
        root = vehicle.rootVehicle
    end

    local trailerCurrentSum, trailerTargetSum, trailerCount = 0, 0, 0
    local overallCurrentSum = localStats.currentSum
    local overallTargetSum = localStats.targetSum
    local overallCount = localStats.count
    if vehicle == root then
        for _, child in ipairs(self:getCombination(root)) do
            if child ~= vehicle then
                local childStats = collectVehicle(child)
                trailerCurrentSum = trailerCurrentSum + childStats.currentSum
                trailerTargetSum = trailerTargetSum + childStats.targetSum
                trailerCount = trailerCount + childStats.count
                overallCurrentSum = overallCurrentSum + childStats.currentSum
                overallTargetSum = overallTargetSum + childStats.targetSum
                overallCount = overallCount + childStats.count
            end
        end
    else
        trailerCurrentSum = localStats.currentSum
        trailerTargetSum = localStats.targetSum
        trailerCount = localStats.count
    end

    local trailerPressure = trailerCount > 0
        and trailerCurrentSum/trailerCount or self.roadPressure
    local trailerTarget = trailerCount > 0
        and trailerTargetSum/trailerCount or self.roadPressure
    local overallTarget = overallCount > 0
        and overallTargetSum/overallCount or localTarget
    local middle = (self.fieldPressure+self.roadPressure)*0.5
    local function getMode(target)
        return (tonumber(target) or self.roadPressure) <= middle and 1 or 0
    end

    local values = {
        pressure=localCurrent,
        frontPressure=frontPressure,
        rearPressure=rearPressure,
        trailerPressure=trailerPressure,
        frontMode=getMode(frontTarget),
        rearMode=getMode(rearTarget),
        trailerMode=getMode(trailerTarget),
        mode=getMode(overallTarget)
    }
    self._pressureDisplayStats[vehicle] = {time=now, values=values}
    return values
end

function TirePressureSystem:updateVehicle(vehicle,dt)
    if tirePressureVehicleDeleting(vehicle) then return end
    self:ensureVehicleSavegamePressureApplied(vehicle)

    local state=stateFor(vehicle)
    if state==nil then return end
    local delta=self.changeRate*math.max(0,dt or 0)*0.001
    if state.current<state.target then state.current=math.min(state.target,state.current+delta)
    else state.current=math.max(state.target,state.current-delta) end

    local sum, targetSum, count = 0, 0, 0
    local wheels=vehicle.spec_wheels and vehicle.spec_wheels.wheels
    for wheelIndex,wheel in pairs(wheels or {}) do
        local wp=wheel.physics
        local ws=wheelStateFor(vehicle,wheel)
        if ws~=nil then
            if ws.current<ws.target then ws.current=math.min(ws.target,ws.current+delta)
            else ws.current=math.max(ws.target,ws.current-delta) end
            sum=sum+ws.current; targetSum=targetSum+(ws.target or ws.current); count=count+1
            self:updateWheelPressureSound(vehicle, wheelIndex, wheel, ws)
        end
        if wp~=nil then
            local p = ws ~= nil and clamp((ws.current-self.minPressure)/math.max(0.01,self.maxPressure-self.minPressure),0,1) or self:getPressure01(vehicle)
            wp.__tpOrigRadius=wp.__tpOrigRadius or wp.__mpOrigRadius or wp.__fgOrigRadius
                or wp.__uytOrigRadius or wp.radiusOriginal or wp.radius
            local desiredRadius = self.enabled and self:getRadiusFromPressure01(wp.__tpOrigRadius, p) or nil
            wp.__tpDesiredRadius=desiredRadius
            if _G.__MudRadiusCombiner~=nil then _G.__MudRadiusCombiner.apply(wp,0.001) end
            local visualPressure = self.enabled and p or 1
            if PunctureSystem ~= nil and PunctureSystem.isWheelPunctured ~= nil
                and PunctureSystem:isWheelPunctured(vehicle, wheelIndex) then
                visualPressure = 0
            end
            self:applyWheelVisualPressure(wheel, visualPressure)
        end
    end

    if count > 0 then
        state.current = sum / count
        state.target = targetSum / count
    end
end

function TirePressureSystem:onUpdate(dt)
    if self.panelOpen and self:getControlledVehicle() == nil then
        self:setPanelOpen(false)
    elseif self.panelOpen then
        -- Re-apply when the player changes the active camera while the panel
        -- remains open; newly registered look events are captured as well.
        self:updatePanelCameraInputBlock()
    end
end

function TirePressureSystem:update(dt)
    self:onUpdate(dt)
end

function TirePressureSystem:installFrictionHook()
    if self._frictionHooked or WheelPhysics==nil or WheelPhysics.updateTireFriction==nil then return end
    self._frictionHooked=true
    WheelPhysics.updateTireFriction=Utils.overwrittenFunction(WheelPhysics.updateTireFriction,function(wp,superFunc,...)
        local old=wp and wp.frictionScale
        local applied=false
        if TirePressureSystem.enabled and wp~=nil and wp.vehicle~=nil then
            local mud=0
            if MudPhysics~=nil and wp.wheel~=nil then
                local x,_,z=MudPhysics:getWheelContactPos(wp.wheel)
                if x~=nil then mud=select(1,MudPhysics:getMudFactorCached(wp.wheel,x,z)) or 0 end
            end
            local mul
            if mud>=((MudPhysics and MudPhysics.mudMinToAffect) or 0.1) then
                mul=TirePressureSystem:getMudGripMultiplier(wp)
            else
                mul=TirePressureSystem:getDryFrictionMultiplier(wp)
            end
            wp.frictionScale=(old or 1)*mul; applied=true
        end
        if superFunc~=nil then superFunc(wp,...) end
        if applied then wp.frictionScale=old end
    end)
end

function TirePressureSystem:loadMap()
    self:installFrictionHook()
    self:createPanelBackgroundOverlays()
    print("[MudSystemPhysics] Tire pressure system loaded")
end

function TirePressureSystem:deleteMap()
    if self.panelOpen then
        self:setPanelOpen(false)
    else
        self:restorePanelCameraInput()
    end
    self:deletePanelBackgroundOverlays()
    if MudSystemWorldSounds ~= nil then
        for vehicle in pairs(self._states) do
            local wheels = vehicle ~= nil and vehicle.spec_wheels ~= nil and vehicle.spec_wheels.wheels or nil
            for wheelIndex in pairs(wheels or {}) do
                MudSystemWorldSounds:stopLoop(vehicle, "pressure:" .. tostring(wheelIndex))
            end
        end
    end
    self._states=setmetatable({},{__mode="k"})
    self._wheelStates=setmetatable({},{__mode="k"})
    self._pressureDisplayStats=setmetatable({},{__mode="k"})
    self.panelOpen=false
    self._dragSlider=nil
end

function TirePressureSystem:deletePanelBackgroundOverlays()
    if self.panelBgOverlay ~= nil then self.panelBgOverlay:delete(); self.panelBgOverlay = nil end
end

function TirePressureSystem:createPanelBackgroundOverlays()
    self:deletePanelBackgroundOverlays()

    local alpha = self.panelBgAlpha or 0.88
    if fileExists == nil or fileExists(self.panelBgPath) then
        self.panelBgOverlay = Overlay.new(self.panelBgPath, 0, 0, 0.01, 0.01)
        self.panelBgOverlay:setColor(1, 1, 1, alpha)
    end
end

function TirePressureSystem:drawPanelBackground(x, y, w, h)
    if self.panelBgOverlay ~= nil then
        self.panelBgOverlay:setPosition(x, y)
        self.panelBgOverlay:setDimension(w, h)
        self.panelBgOverlay:render()
    else
        drawFilledRect(x, y, w, h, 0.018, 0.023, 0.028, self.panelBgAlpha or 0.88)
    end
end

function TirePressureSystem:drawButton(rect, text, enabled)
    local r = self.uiGreenR or 0.48
    local g = self.uiGreenG or 0.76
    local b = self.uiGreenB or 0.02

    drawFilledRect(rect.x, rect.y, rect.w, rect.h, enabled == false and 0.10 or r, enabled == false and 0.10 or g, enabled == false and 0.10 or b, enabled == false and 0.45 or 0.90)
    drawFilledRect(rect.x, rect.y + rect.h - 0.0015, rect.w, 0.0015, 1, 1, 1, 0.18)
    setTextAlignment(RenderText.ALIGN_CENTER)
    if enabled == false then
        setTextColor(0.72,0.76,0.78,0.55)
    else
        setTextColor(0.015,0.025,0.010,0.96)
    end
    renderText(rect.x + rect.w * 0.5, rect.y + rect.h * 0.28, 0.0105, text)
end

local TP_PANEL_FRAME_W = 0.335
local TP_PANEL_COLUMN_GAP = 0.012
local TP_PANEL_FRAME_GAP = 0.010
local TP_PANEL_ROW_H = 0.031
local TP_PANEL_FRAME_BASE_H = 0.046
local TP_PANEL_MAX_CONTENT_H = 0.720

function TirePressureSystem:buildPanelPages(groups)
    local pages = {}

    local function newPage()
        local page = {
            columns = {
                {items={}, height=0},
                {items={}, height=0}
            }
        }
        pages[#pages+1] = page
        return page
    end

    local page = newPage()
    for _, group in ipairs(groups or {}) do
        local wheelCount = #group.entries
        local rowH = TP_PANEL_ROW_H
        if wheelCount > 0 then
            rowH = math.min(
                rowH,
                (TP_PANEL_MAX_CONTENT_H - TP_PANEL_FRAME_BASE_H) / wheelCount
            )
        end

        group.panelRowH = rowH
        group.panelFrameH = TP_PANEL_FRAME_BASE_H + wheelCount * rowH

        local firstColumn = page.columns[1].height <= page.columns[2].height and 1 or 2
        local secondColumn = firstColumn == 1 and 2 or 1
        local chosenColumn = nil

        for _, columnIndex in ipairs({firstColumn, secondColumn}) do
            local column = page.columns[columnIndex]
            local required = group.panelFrameH
                + (column.height > 0 and TP_PANEL_FRAME_GAP or 0)
            if column.height + required <= TP_PANEL_MAX_CONTENT_H + 0.0001 then
                chosenColumn = columnIndex
                break
            end
        end

        if chosenColumn == nil then
            page = newPage()
            chosenColumn = 1
        end

        local column = page.columns[chosenColumn]
        if column.height > 0 then
            column.height = column.height + TP_PANEL_FRAME_GAP
        end
        column.items[#column.items+1] = group
        column.height = column.height + group.panelFrameH
    end

    return pages
end

function TirePressureSystem:drawWheelFrame(group, frame, isSelected)
    local r = self.uiGreenR or 0.48
    local g = self.uiGreenG or 0.76
    local b = self.uiGreenB or 0.02
    local borderAlpha = isSelected and 0.94 or 0.18
    local borderSize = isSelected and 0.0020 or 0.0012

    drawFilledRect(frame.x, frame.y, frame.w, frame.h, 0.025, 0.032, 0.038, 0.94)
    drawFilledRect(frame.x, frame.y + frame.h - borderSize, frame.w, borderSize, r, g, b, borderAlpha)
    drawFilledRect(frame.x, frame.y, frame.w, borderSize, r, g, b, borderAlpha)
    drawFilledRect(frame.x, frame.y, borderSize, frame.h, r, g, b, borderAlpha)
    drawFilledRect(frame.x + frame.w - borderSize, frame.y, borderSize, frame.h, r, g, b, borderAlpha)

    local headerH = 0.036
    local headerY = frame.y + frame.h - headerH
    drawFilledRect(frame.x+borderSize, headerY, frame.w-borderSize*2, headerH-borderSize, 0.045, 0.058, 0.066, 0.92)

    local buttonW = 0.052
    local buttonGap = 0.005
    local buttonH = 0.021
    local buttonY = headerY + (headerH-buttonH)*0.5
    local road = {
        x=frame.x+frame.w-0.008-buttonW,
        y=buttonY,
        w=buttonW,
        h=buttonH,
        kind="vehiclePreset",
        vehicle=group.vehicle,
        pressure=self.roadPressure
    }
    local field = {
        x=road.x-buttonGap-buttonW,
        y=buttonY,
        w=buttonW,
        h=buttonH,
        kind="vehiclePreset",
        vehicle=group.vehicle,
        pressure=self.fieldPressure
    }

    self:drawButton(field, g_i18n:getText("mudsys_tp_field"), true)
    self:drawButton(road, g_i18n:getText("mudsys_tp_road"), true)
    self._panelRects[#self._panelRects+1] = field
    self._panelRects[#self._panelRects+1] = road

    local titleX = frame.x + 0.012
    local titleMaxW = math.max(0.04, field.x-titleX-0.006)
    local title = group.name
    if Utils ~= nil and Utils.limitTextToWidth ~= nil then
        title = Utils.limitTextToWidth(title, 0.0115, titleMaxW, false, "...")
    end

    setTextAlignment(RenderText.ALIGN_LEFT)
    setTextBold(true)
    if isSelected then
        setTextColor(r, g, b, 1)
    else
        setTextColor(0.94, 0.96, 0.97, 0.98)
    end
    renderText(titleX, headerY+headerH*0.31, 0.0115, title)
    setTextBold(false)

    local rowH = group.panelRowH or TP_PANEL_ROW_H
    for index, entry in ipairs(group.entries) do
        local rowY = frame.y + frame.h - headerH - index * rowH
        local state = entry.state
        local target = state ~= nil and state.target or self.roadPressure
        local progress = clamp(
            (target-self.minPressure) / math.max(0.01, self.maxPressure-self.minPressure),
            0,
            1
        )

        drawFilledRect(
            frame.x+0.006,
            rowY+0.002,
            frame.w-0.012,
            math.max(0.012, rowH-0.004),
            0.055,
            0.064,
            0.070,
            index%2 == 0 and 0.72 or 0.55
        )

        if index > 1 and entry.panelGroup ~= group.entries[index-1].panelGroup then
            drawFilledRect(
                frame.x+0.010,
                rowY+rowH-0.0012,
                frame.w-0.020,
                0.0012,
                r,
                g,
                b,
                0.58
            )
        end

        setTextAlignment(RenderText.ALIGN_LEFT)
        setTextColor(0.92, 0.95, 0.96, 0.95)
        renderText(
            frame.x+0.012,
            rowY+rowH*0.31,
            0.0096,
            entry.label
        )

        local minus = {
            x=frame.x+frame.w-0.055,
            y=rowY+(rowH-0.020)*0.5,
            w=0.021,
            h=0.020,
            kind="step",
            entry=entry,
            delta=-self.step
        }
        local plus = {
            x=frame.x+frame.w-0.029,
            y=minus.y,
            w=0.021,
            h=0.020,
            kind="step",
            entry=entry,
            delta=self.step
        }

        local bar = {
            x=frame.x+0.094,
            y=rowY+rowH*0.44,
            w=math.max(0.055, minus.x-frame.x-0.150),
            h=0.006,
            kind="slider",
            entry=entry
        }
        drawFilledRect(bar.x, bar.y, bar.w, bar.h, 1, 1, 1, 0.13)
        drawFilledRect(bar.x, bar.y, bar.w*progress, bar.h, r, g, b, 0.94)
        drawFilledRect(bar.x+bar.w*progress-0.003, bar.y-0.004, 0.006, 0.014, 1, 1, 1, 0.90)
        self._panelRects[#self._panelRects+1] = bar

        setTextAlignment(RenderText.ALIGN_RIGHT)
        setTextColor(1, 1, 1, 0.94)
        renderText(minus.x-0.006, rowY+rowH*0.30, 0.0095, string.format("%.1f", target))

        self:drawButton(minus, "-", true)
        self:drawButton(plus, "+", true)
        self._panelRects[#self._panelRects+1] = minus
        self._panelRects[#self._panelRects+1] = plus
    end
end

function TirePressureSystem:drawPanel(vehicle)
    local groups = self:getWheelGroups(vehicle)
    local pages = self:buildPanelPages(groups)
    local pageCount = math.max(1, #pages)
    self._panelPage = math.floor(clamp(self._panelPage or 1, 1, pageCount))
    self._panelPageCount = pageCount

    local page = pages[self._panelPage]
    local hasSecondColumn = page ~= nil and #page.columns[2].items > 0
    local columnCount = hasSecondColumn and 2 or 1
    local outerPad = 0.010
    local headerH = 0.040
    local headerGap = 0.008
    local navH = pageCount > 1 and 0.031 or 0
    local contentH = page ~= nil
        and math.max(0.070, page.columns[1].height, page.columns[2].height)
        or 0.070
    local w = outerPad*2 + columnCount*TP_PANEL_FRAME_W + (columnCount-1)*TP_PANEL_COLUMN_GAP
    local h = outerPad*2 + headerH + headerGap + contentH + navH
    local x = 0.5 - w*0.5
    local y = 0.5 - h*0.5

    self._panelRects = {}
    self:drawPanelBackground(x, y, w, h)

    local headerY = y+h-outerPad-headerH
    drawFilledRect(x+outerPad, headerY, w-outerPad*2, headerH, 0.04, 0.055, 0.065, 0.82)

    setTextAlignment(RenderText.ALIGN_CENTER)
    setTextBold(true)
    setTextColor(1, 1, 1, 0.98)
    renderText(x+w*0.5, headerY+headerH*0.32, 0.0155, g_i18n:getText("mudsys_tp_panel_title"))
    setTextBold(false)

    local closeRect = {
        x=x+w-outerPad-0.027,
        y=headerY+headerH*0.5-0.0105,
        w=0.021,
        h=0.021,
        kind="close"
    }
    self:drawButton(closeRect, "X", true)
    self._panelRects[#self._panelRects+1] = closeRect

    if #groups == 0 then
        setTextAlignment(RenderText.ALIGN_CENTER)
        setTextColor(1, 1, 1, 0.75)
        renderText(x+w*0.5, y+outerPad+0.020, 0.013, g_i18n:getText("mudsys_tp_no_wheels"))
        return
    end

    local selectedVehicle = self:getSelectedPressureVehicle(vehicle)
    local contentTop = headerY-headerGap
    for columnIndex=1,columnCount do
        local column = page.columns[columnIndex]
        local frameX = x+outerPad+(columnIndex-1)*(TP_PANEL_FRAME_W+TP_PANEL_COLUMN_GAP)
        local frameTop = contentTop
        for _, group in ipairs(column.items) do
            local frame = {
                x=frameX,
                y=frameTop-group.panelFrameH,
                w=TP_PANEL_FRAME_W,
                h=group.panelFrameH
            }
            self:drawWheelFrame(group, frame, selectedVehicle == group.vehicle)
            frameTop = frame.y-TP_PANEL_FRAME_GAP
        end
    end

    if pageCount > 1 then
        local navY = y+outerPad*0.55
        local prev = {x=0.5-0.070,y=navY,w=0.030,h=0.024,kind="page",delta=-1}
        local nextPage = {x=0.5+0.040,y=navY,w=0.030,h=0.024,kind="page",delta=1}
        self:drawButton(prev, "<", self._panelPage > 1)
        self:drawButton(nextPage, ">", self._panelPage < pageCount)
        self._panelRects[#self._panelRects+1] = prev
        self._panelRects[#self._panelRects+1] = nextPage

        setTextAlignment(RenderText.ALIGN_CENTER)
        setTextColor(0.92, 0.95, 0.96, 0.95)
        renderText(0.5, navY+0.006, 0.011, string.format("%d / %d", self._panelPage, pageCount))
    end
end

function TirePressureSystem:draw()
    local mission=g_currentMission
    if not self.enabled or mission==nil or mission.hud==nil then return end
    if mission.hud.getIsVisible~=nil and not mission.hud:getIsVisible() then return end

    local vehicle = self:getControlledVehicle()
    if vehicle == nil then
        if self.panelOpen then
            self:setPanelOpen(false)
        end
        return
    end

    if self.panelOpen then
        self:drawPanel(vehicle)
    end

    setTextColor(1,1,1,1)
    setTextBold(false)
end

local function hit(rect, x, y)
    return rect ~= nil and x >= rect.x and x <= rect.x + rect.w and y >= rect.y and y <= rect.y + rect.h
end

function TirePressureSystem:setEntryPressure(entry, pressure)
    if entry == nil then return end
    TirePressureEvent.send(entry.vehicle, clamp(pressure,self.minPressure,self.maxPressure), entry.wheelIndex)
end

function TirePressureSystem:setEntryFromSlider(entry, posX, rect)
    if entry == nil or rect == nil then return end
    local t = clamp((posX - rect.x) / math.max(0.001, rect.w), 0, 1)
    self:setEntryPressure(entry, self.minPressure + (self.maxPressure - self.minPressure) * t)
end

function TirePressureSystem:mouseEvent(posX, posY, isDown, isUp, button)
    if not self.panelOpen then return end
    if self:getControlledVehicle() == nil then
        self:setPanelOpen(false)
        return
    end
    if button ~= nil and Input ~= nil and button ~= Input.MOUSE_BUTTON_LEFT then return end

    if isUp then
        self._dragSlider = nil
        return
    end

    if self._dragSlider ~= nil then
        self:setEntryFromSlider(self._dragSlider.entry, posX, self._dragSlider.rect)
        return
    end

    if not isDown then return end

    for _, rect in ipairs(self._panelRects or {}) do
        if hit(rect, posX, posY) then
            if rect.kind == "close" then
                self:setPanelOpen(false)
                return
            elseif rect.kind == "vehiclePreset" then
                if rect.vehicle ~= nil then
                    TirePressureEvent.send(rect.vehicle, rect.pressure)
                end
                return
            elseif rect.kind == "page" then
                local pageCount = math.max(1, self._panelPageCount or 1)
                self._panelPage = math.floor(clamp(
                    (self._panelPage or 1) + (rect.delta or 0),
                    1,
                    pageCount
                ))
                self._dragSlider = nil
                return
            elseif rect.kind == "step" then
                local current = rect.entry ~= nil and rect.entry.state ~= nil and rect.entry.state.target or self.roadPressure
                self:setEntryPressure(rect.entry, current + (rect.delta or 0))
                return
            elseif rect.kind == "slider" then
                self._dragSlider = {entry=rect.entry, rect=rect}
                self:setEntryFromSlider(rect.entry, posX, rect)
                return
            end
        end
    end
end

function TirePressureSystem:onSettingsChanged()
    self.enabled=self.enabled==true
    if not self.enabled and self.panelOpen then
        self:setPanelOpen(false)
    end
    self.lowPressureMudGrip = clamp(self.lowPressureMudGrip, 1.0, 2.5)
    self.highPressureMudGrip = clamp(self.highPressureMudGrip, 0.5, 1.2)
    self.lowPressureSinkMul = clamp(self.lowPressureSinkMul, 0.25, 1.0)
    self.highPressureSinkMul = clamp(self.highPressureSinkMul, 1.0, 1.8)
    self.lowPressureMudBrakeMul = clamp(self.lowPressureMudBrakeMul, 0.25, 1.0)
    self.highPressureMudBrakeMul = clamp(self.highPressureMudBrakeMul, 1.0, 1.8)
    self.lowPressureRadiusSinkMul = clamp(self.lowPressureRadiusSinkMul, 0.10, 1.0)
    self.highPressureRadiusSinkMul = clamp(self.highPressureRadiusSinkMul, 1.0, 2.0)
    self.lowPressureSinkOutMul = clamp(self.lowPressureSinkOutMul, 1.0, 2.0)
    self.highPressureSinkOutMul = clamp(self.highPressureSinkOutMul, 0.5, 1.0)
    self.lowPressureMudSpeedLimitMul = clamp(self.lowPressureMudSpeedLimitMul, 1.0, 2.0)
    self.highPressureMudSpeedLimitMul = clamp(self.highPressureMudSpeedLimitMul, 0.5, 1.0)
    self.lowPressureMotorLoadMul = clamp(self.lowPressureMotorLoadMul, 0.25, 1.0)
    self.highPressureMotorLoadMul = clamp(self.highPressureMotorLoadMul, 1.0, 1.8)
    if not self.enabled then
        for vehicle,_ in pairs(self._states) do self:updateVehicle(vehicle,0) end
    end
end

addModEventListener(TirePressureSystem)
