if not TowBarMod then TowBarMod = {} end
if not TowBarMod.Utils then TowBarMod.Utils = {} end
if not TowBarMod.UI then TowBarMod.UI = {} end

local LANDTRAIN_DEBUG = true
local LANDTRAIN_FALLBACK_MAX_DIST_SQ = 3.25 -- ~1.8 tiles
local LANDTRAIN_FALLBACK_MAX_DZ = 0.9
local LANDTRAIN_FRONT_SQL_ID_KEY = "landtrainTowbarFrontSqlId"
local LANDTRAIN_FRONT_ATTACHMENT_A_KEY = "landtrainTowbarAttachmentA"
local LANDTRAIN_FRONT_ATTACHMENT_B_KEY = "landtrainTowbarAttachmentB"
local LANDTRAIN_REAR_SQL_ID_KEY = "landtrainTowbarRearSqlId"
local LANDTRAIN_SAVED_POS_X_KEY = "landtrainSavedPosX"
local LANDTRAIN_SAVED_POS_Y_KEY = "landtrainSavedPosY"
local LANDTRAIN_SAVED_POS_Z_KEY = "landtrainSavedPosZ"
local LANDTRAIN_SAVED_DIR_KEY = "landtrainSavedDir"
local LANDTRAIN_ATTACHMENT_BASE_OFFSETS = {}

local function emitLandtrainLog(line)
    print(line)
    if type(writeLog) == "function" then
        pcall(writeLog, "Landtrain", line)
    end
end

local function ltLog(msg)
    if not LANDTRAIN_DEBUG then return end
    emitLandtrainLog("[Landtrain] " .. tostring(msg))
end

local function ltInfo(msg)
    emitLandtrainLog("[Landtrain][Info] " .. tostring(msg))
end

ltInfo("UnlimitedTowbarChains loaded. debug=" .. tostring(LANDTRAIN_DEBUG))

local function vehLabel(vehicle)
    if vehicle == nil then return "nil" end
    local name = vehicle:getScriptName() or "unknown"
    return tostring(vehicle:getId()) .. ":" .. tostring(name)
end

local _landtrainPairPickPosA = Vector3f.new()
local _landtrainPairPickPosB = Vector3f.new()

local function hasAttachmentById(vehicle, attachmentId)
    if vehicle == nil or attachmentId == nil then return false end
    local script = vehicle:getScript()
    if script == nil then return false end
    return script:getAttachmentById(attachmentId) ~= nil
end

local function isUsableAttachmentPair(towingVehicle, towedVehicle, attachmentA, attachmentB)
    if attachmentA == nil or attachmentB == nil or attachmentA == attachmentB then
        return false
    end
    return hasAttachmentById(towingVehicle, attachmentA) and hasAttachmentById(towedVehicle, attachmentB)
end

local function getAttachmentBaseOffsetKey(script, attachmentId)
    if script == nil or attachmentId == nil then return nil end
    local scriptName = script.getName and script:getName() or tostring(script)
    return tostring(scriptName) .. "|" .. tostring(attachmentId)
end

local function cacheAttachmentBaseOffset(script, attachmentId)
    local key = getAttachmentBaseOffsetKey(script, attachmentId)
    if key == nil or LANDTRAIN_ATTACHMENT_BASE_OFFSETS[key] ~= nil then
        return
    end
    local attachment = script and script:getAttachmentById(attachmentId) or nil
    if attachment == nil then
        return
    end
    local offset = attachment:getOffset()
    LANDTRAIN_ATTACHMENT_BASE_OFFSETS[key] = {
        x = offset:x(),
        y = offset:y(),
        z = offset:z()
    }
end

local function getAttachmentBaseOffset(script, attachmentId)
    local key = getAttachmentBaseOffsetKey(script, attachmentId)
    if key == nil then return nil end
    return LANDTRAIN_ATTACHMENT_BASE_OFFSETS[key]
end

local function getTowbarStyleAttachmentPair(towingVehicle, towedVehicle)
    local attachmentA = nil
    local attachmentB = nil

    if hasAttachmentById(towingVehicle, "trailer") then
        attachmentA = "trailer"
    elseif towingVehicle ~= nil then
        attachmentA = towingVehicle:getTowAttachmentSelf()
    end

    if hasAttachmentById(towedVehicle, "trailerfront") then
        attachmentB = "trailerfront"
    elseif towedVehicle ~= nil then
        attachmentB = towedVehicle:getTowAttachmentSelf()
    end

    if attachmentA == attachmentB then
        if attachmentA == "trailer" and hasAttachmentById(towedVehicle, "trailerfront") then
            attachmentB = "trailerfront"
        elseif attachmentA == "trailerfront" and hasAttachmentById(towingVehicle, "trailer") then
            attachmentA = "trailer"
        end
    end

    if attachmentA == nil then attachmentA = "trailer" end
    if attachmentB == nil then attachmentB = "trailerfront" end
    return attachmentA, attachmentB
end

local function chooseLandtrainAttachmentPair(towingVehicle, towedVehicle, preferredA, preferredB)
    local seen = {}
    local candidates = {}
    local function addCandidate(a, b)
        if a == nil or b == nil or a == b then return end
        local key = tostring(a) .. "|" .. tostring(b)
        if seen[key] then return end
        seen[key] = true
        table.insert(candidates, { a = a, b = b })
    end

    addCandidate(preferredA, preferredB)
    addCandidate(towingVehicle and towingVehicle:getTowAttachmentSelf() or nil, towedVehicle and towedVehicle:getTowAttachmentSelf() or nil)
    addCandidate("trailer", "trailerfront")
    addCandidate("trailerfront", "trailer")

    local bestPair = nil
    local bestDistSq = nil
    local bestDz = nil
    for _, pair in ipairs(candidates) do
        if hasAttachmentById(towingVehicle, pair.a) and hasAttachmentById(towedVehicle, pair.b) then
            local posA = towingVehicle:getAttachmentWorldPos(pair.a, _landtrainPairPickPosA)
            local posB = towedVehicle:getAttachmentWorldPos(pair.b, _landtrainPairPickPosB)
            if posA ~= nil and posB ~= nil then
                local dx = posA:x() - posB:x()
                local dy = posA:y() - posB:y()
                local dz = math.abs(posA:z() - posB:z())
                local distSq = dx * dx + dy * dy + (posA:z() - posB:z()) * (posA:z() - posB:z())
                if bestPair == nil or distSq < bestDistSq or (distSq == bestDistSq and dz < bestDz) then
                    bestPair = pair
                    bestDistSq = distSq
                    bestDz = dz
                end
            elseif bestPair == nil then
                bestPair = pair
            end
        end
    end

    if bestPair ~= nil then
        return bestPair.a, bestPair.b
    end

    local fallbackA = preferredA or (towingVehicle and towingVehicle:getTowAttachmentSelf()) or "trailer"
    local fallbackB = preferredB or (towedVehicle and towedVehicle:getTowAttachmentSelf()) or "trailerfront"
    if fallbackA == fallbackB then
        if fallbackA == "trailerfront" then
            fallbackA = "trailer"
        else
            fallbackB = "trailerfront"
        end
    end
    return fallbackA, fallbackB
end

local function chooseLandtrainTowbarPair(towingVehicle, towedVehicle, preferredA, preferredB)
    if isUsableAttachmentPair(towingVehicle, towedVehicle, preferredA, preferredB) then
        return preferredA, preferredB
    end

    -- Match Towbar's load reapply pair selection before Landtrain-specific fallbacks.
    local towbarA = (towingVehicle and towingVehicle:getTowAttachmentSelf()) or "trailer"
    local towbarB = (towedVehicle and towedVehicle:getTowAttachmentSelf()) or "trailerfront"
    if isUsableAttachmentPair(towingVehicle, towedVehicle, towbarA, towbarB) then
        return towbarA, towbarB
    end

    local styleA, styleB = getTowbarStyleAttachmentPair(towingVehicle, towedVehicle)
    if isUsableAttachmentPair(towingVehicle, towedVehicle, styleA, styleB) then
        return styleA, styleB
    end

    return chooseLandtrainAttachmentPair(towingVehicle, towedVehicle, preferredA, preferredB)
end

local function clearChangedTowedAttachmentOffset(vehicle)
    if vehicle == nil then return end
    local modData = vehicle:getModData()
    if modData == nil or modData["isChangedTowedAttachment"] ~= true then return end

    local script = vehicle:getScript()
    local changedAttachmentId = modData["towBarChangedAttachmentId"]
    if changedAttachmentId == nil and vehicle.getTowAttachmentSelf then
        changedAttachmentId = vehicle:getTowAttachmentSelf()
    end

    if script ~= nil and changedAttachmentId ~= nil then
        cacheAttachmentBaseOffset(script, changedAttachmentId)
        local baseOffset = getAttachmentBaseOffset(script, changedAttachmentId)
        local towedAttachment = script:getAttachmentById(changedAttachmentId)
        if towedAttachment ~= nil then
            if baseOffset ~= nil then
                towedAttachment:getOffset():set(baseOffset.x, baseOffset.y, baseOffset.z)
            else
                local offset = towedAttachment:getOffset()
                local storedShift = tonumber(modData["towBarChangedOffsetZShift"])
                if storedShift ~= nil then
                    towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() - storedShift)
                else
                    local zShift = offset:z() > 0 and -1 or 1
                    towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() + zShift)
                end
            end
        end
    end

    modData["isChangedTowedAttachment"] = false
    modData["towBarChangedAttachmentId"] = nil
    modData["towBarChangedOffsetZShift"] = nil
    vehicle:transmitModData()
end

local function updateAttachmentsForRigidTowNoStack(towingVehicle, towedVehicle, attachmentA, attachmentB)
    if TowBarMod == nil or TowBarMod.Utils == nil or TowBarMod.Utils.updateAttachmentsForRigidTow == nil then
        return false
    end
    local towedScript = towedVehicle and towedVehicle:getScript() or nil
    cacheAttachmentBaseOffset(towedScript, attachmentB)
    clearChangedTowedAttachmentOffset(towedVehicle)
    TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
    return true
end

local function dumpTowState(prefix, vehicle)
    if not LANDTRAIN_DEBUG then return end
    if vehicle == nil then
        ltLog(prefix .. " vehicle=nil")
        return
    end
    local modData = vehicle:getModData() or {}
    local front = vehicle:getVehicleTowedBy()
    local rear = vehicle:getVehicleTowing()
    ltLog(prefix
        .. " v=" .. vehLabel(vehicle)
        .. " front=" .. vehLabel(front)
        .. " rear=" .. vehLabel(rear)
        .. " md.isTowingByTowBar=" .. tostring(modData["isTowingByTowBar"])
        .. " md.towed=" .. tostring(modData["towed"]))
end

local function refreshTowBarState(vehicle)
    if vehicle == nil then return end

    local modData = vehicle:getModData()
    if modData == nil then return end

    local frontVehicle = vehicle:getVehicleTowedBy()
    local rearVehicle = vehicle:getVehicleTowing()

    local isTowedByTowBar = false
    if frontVehicle ~= nil then
        local frontModData = frontVehicle:getModData()
        if modData["towed"] == true or (frontModData and frontModData["isTowingByTowBar"] == true) then
            isTowedByTowBar = true
        end
    end

    local isTowingByTowBar = false
    if rearVehicle ~= nil then
        local rearModData = rearVehicle:getModData()
        if rearModData and rearModData["towed"] == true and rearModData["isTowingByTowBar"] == true then
            isTowingByTowBar = true
        end
    end

    modData["towed"] = isTowedByTowBar
    modData["isTowingByTowBar"] = (isTowedByTowBar or isTowingByTowBar)
    vehicle:transmitModData()
end

local function setTowBarModelVisibleForVehicle(vehicle, visible)
    if vehicle == nil then return end
    local part = vehicle:getPartById("towbar")
    if part == nil then return end

    for j = 0, 23 do
        part:setModelVisible("towbar" .. j, false)
    end

    if visible then
        local z = vehicle:getScript():getPhysicsChassisShape():z() / 2 - 0.1
        local index = math.floor((z * 2 / 3 - 1) * 10)
        if index < 0 then index = 0 end
        if index > 23 then index = 23 end
        part:setModelVisible("towbar" .. index, true)
    end

    vehicle:doDamageOverlay()
end

local function getVehicleByIdSafe(vehicleId)
    if vehicleId == nil then return nil end
    if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.getVehicleByIdSafe then
        return TowBarMod.Hook.getVehicleByIdSafe(vehicleId)
    end
    if getVehicleById then
        return getVehicleById(vehicleId)
    end

    local cell = getCell()
    if cell == nil then return nil end
    local vehicles = cell:getVehicles()
    if vehicles == nil then return nil end

    for i = 0, vehicles:size() - 1 do
        local vehicle = vehicles:get(i)
        if vehicle ~= nil and vehicle:getId() == vehicleId then
            return vehicle
        end
    end
    return nil
end

local function getVehicleBySqlIdSafe(sqlId)
    if sqlId == nil then return nil end
    local targetSqlId = tonumber(sqlId)
    if targetSqlId == nil or targetSqlId < 0 then
        return nil
    end

    local cell = getCell()
    if cell == nil then return nil end
    local vehicles = cell:getVehicles()
    if vehicles == nil then return nil end

    for i = 0, vehicles:size() - 1 do
        local vehicle = vehicles:get(i)
        if vehicle ~= nil and vehicle.getSqlId and vehicle:getSqlId() == targetSqlId then
            return vehicle
        end
    end
    return nil
end

local function storeLandtrainFrontLinkData(towingVehicle, towedVehicle, attachmentA, attachmentB)
    if towingVehicle == nil or towedVehicle == nil then return end
    local towingModData = towingVehicle:getModData()
    local towedModData = towedVehicle:getModData()
    if towingModData == nil or towedModData == nil then return end
    if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then
        towingModData[LANDTRAIN_REAR_SQL_ID_KEY] = nil
        towingVehicle:transmitModData()
        towedModData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil
        towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil
        towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil
        towedVehicle:transmitModData()
        ltLog("storeLandtrainFrontLinkData rejected self-link vehicle=" .. vehLabel(towedVehicle))
        return
    end

    local towingSqlId = towingVehicle.getSqlId and towingVehicle:getSqlId() or -1
    if towingSqlId ~= nil and towingSqlId >= 0 then
        towedModData[LANDTRAIN_FRONT_SQL_ID_KEY] = towingSqlId
    else
        towedModData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil
    end
    local towedSqlId = towedVehicle.getSqlId and towedVehicle:getSqlId() or -1
    if towedSqlId ~= nil and towedSqlId >= 0 then
        towingModData[LANDTRAIN_REAR_SQL_ID_KEY] = towedSqlId
    else
        towingModData[LANDTRAIN_REAR_SQL_ID_KEY] = nil
    end
    towingVehicle:transmitModData()
    local preferredA = attachmentA or towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY]
    local preferredB = attachmentB or towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY]
    local resolvedA, resolvedB = chooseLandtrainTowbarPair(towingVehicle, towedVehicle, preferredA, preferredB)
    towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = resolvedA
    towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = resolvedB
    towedVehicle:transmitModData()
end

local function clearLandtrainFrontLinkData(vehicle)
    if vehicle == nil then return end
    local modData = vehicle:getModData()
    if modData == nil then return end
    local frontSqlId = tonumber(modData[LANDTRAIN_FRONT_SQL_ID_KEY])
    local vehicleSqlId = vehicle.getSqlId and vehicle:getSqlId() or nil
    if frontSqlId ~= nil and vehicleSqlId ~= nil and vehicleSqlId >= 0 then
        local frontVehicle = getVehicleBySqlIdSafe(frontSqlId)
        if frontVehicle ~= nil then
            local frontModData = frontVehicle:getModData()
            if frontModData ~= nil and tonumber(frontModData[LANDTRAIN_REAR_SQL_ID_KEY]) == vehicleSqlId then
                frontModData[LANDTRAIN_REAR_SQL_ID_KEY] = nil
                frontVehicle:transmitModData()
            end
        end
    end
    modData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil
    modData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil
    modData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil
    vehicle:transmitModData()
end

local function isTowbarManagedVehicle(vehicle)
    if vehicle == nil then return false end
    local modData = vehicle:getModData()
    if modData == nil then return false end
    return modData["isTowingByTowBar"] == true
        or modData["towed"] == true
        or modData.towBarOriginalScriptName ~= nil
        or modData.towBarOriginalMass ~= nil
        or modData.towBarOriginalBrakingForce ~= nil
        or modData["isChangedTowedAttachment"] == true
        or modData["towBarChangedAttachmentId"] ~= nil
        or modData[LANDTRAIN_FRONT_SQL_ID_KEY] ~= nil
        or modData[LANDTRAIN_REAR_SQL_ID_KEY] ~= nil
end

local function restoreVehicleTowbarDefaults(vehicle)
    if vehicle == nil then return end
    local modData = vehicle:getModData()
    if modData == nil then return end

    local script = vehicle:getScript()
    local towingAttachmentId = vehicle:getTowAttachmentSelf()
    if script and towingAttachmentId then
        local towingAttachment = script:getAttachmentById(towingAttachmentId)
        if towingAttachment ~= nil then
            towingAttachment:setUpdateConstraint(true)
            towingAttachment:setZOffset((towingAttachmentId == "trailer") and -1 or 1)
        end
    end

    local changedAttachmentId = modData["towBarChangedAttachmentId"] or towingAttachmentId
    if script and changedAttachmentId then
        local towedAttachment = script:getAttachmentById(changedAttachmentId)
        if towedAttachment ~= nil then
            towedAttachment:setUpdateConstraint(true)
            towedAttachment:setZOffset((changedAttachmentId == "trailer") and -1 or 1)

            if modData["isChangedTowedAttachment"] then
                local offset = towedAttachment:getOffset()
                local storedShift = tonumber(modData["towBarChangedOffsetZShift"])
                if storedShift ~= nil then
                    towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() - storedShift)
                else
                    local zShift = offset:z() > 0 and -1 or 1
                    towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() + zShift)
                end
            end
        end
    end

    if modData.towBarOriginalScriptName then
        vehicle:setScriptName(modData.towBarOriginalScriptName)
    end
    if modData.towBarOriginalMass ~= nil then
        vehicle:setMass(modData.towBarOriginalMass)
    end
    if modData.towBarOriginalBrakingForce ~= nil then
        vehicle:setBrakingForce(modData.towBarOriginalBrakingForce)
    end
    vehicle:constraintChanged()
    vehicle:updateTotalMass()

    modData["towed"] = false
    modData["isChangedTowedAttachment"] = false
    modData["towBarChangedAttachmentId"] = nil
    modData["towBarChangedOffsetZShift"] = nil
    modData.towBarOriginalScriptName = nil
    modData.towBarOriginalMass = nil
    modData.towBarOriginalBrakingForce = nil
    modData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil
    modData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil
    modData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil
    modData[LANDTRAIN_REAR_SQL_ID_KEY] = nil

    if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.clearReapplied then
        TowBarMod.Hook.clearReapplied(vehicle)
    end
    if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.lastForcedReapplyAtByVehicle then
        TowBarMod.Hook.lastForcedReapplyAtByVehicle[vehicle:getId()] = nil
    end
    setTowBarModelVisibleForVehicle(vehicle, false)
end

local function reconcileTowbarSplitVehicle(vehicle)
    if vehicle == nil then return end
    if not isTowbarManagedVehicle(vehicle) then return end

    local modData = vehicle:getModData()
    if modData == nil then return end

    local frontVehicle = vehicle:getVehicleTowedBy()
    local rearVehicle = vehicle:getVehicleTowing()
    local rearModData = rearVehicle and rearVehicle:getModData() or nil
    local hasTowbarRear = rearModData and rearModData["towed"] == true and rearModData["isTowingByTowBar"] == true

    if frontVehicle == nil and (modData["towed"] == true or modData.towBarOriginalMass ~= nil or modData.towBarOriginalBrakingForce ~= nil) then
        ltLog("reconcileTowbarSplitVehicle restoring new lead " .. vehLabel(vehicle))
        restoreVehicleTowbarDefaults(vehicle)
    elseif frontVehicle ~= nil and (modData["isTowingByTowBar"] == true or modData["towed"] == true) then
        storeLandtrainFrontLinkData(frontVehicle, vehicle)
    end

    modData["towed"] = (frontVehicle ~= nil)
    modData["isTowingByTowBar"] = (modData["towed"] == true) or (hasTowbarRear == true)
    vehicle:transmitModData()
end

local function reconcileTowbarSplitAround(vehicle)
    if vehicle == nil then return end
    reconcileTowbarSplitVehicle(vehicle)
    reconcileTowbarSplitVehicle(vehicle:getVehicleTowedBy())
    reconcileTowbarSplitVehicle(vehicle:getVehicleTowing())
end

local function queueTowbarSplitReconcile(playerObj, vehicle, delayTicks)
    if playerObj == nil or vehicle == nil then return end
    local delay = delayTicks or 12
    ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, delay, function(_, vehicleArg)
        reconcileTowbarSplitAround(vehicleArg)
    end, vehicle))
end

local function getLoadedVehicles()
    local vehicles = {}
    local cell = getCell()
    if cell == nil then return vehicles end
    local list = cell:getVehicles()
    if list == nil then return vehicles end
    for i = 0, list:size() - 1 do
        local vehicle = list:get(i)
        if vehicle ~= nil then
            table.insert(vehicles, vehicle)
        end
    end
    return vehicles
end

local function breakConstraintSafe(vehicle, reason)
    if vehicle == nil then return false end
    ltLog("breakConstraintSafe reason=" .. tostring(reason) .. " vehicle=" .. vehLabel(vehicle))
    local ok = pcall(function()
        vehicle:breakConstraint(true, false)
    end)
    if ok then return true end

    local playerObj = getSpecificPlayer(0)
    if playerObj ~= nil then
        local towingVehicle = vehicle
        local towedVehicle = vehicle:getVehicleTowing()
        if towedVehicle == nil then
            local frontVehicle = vehicle:getVehicleTowedBy()
            if frontVehicle ~= nil then
                towingVehicle = frontVehicle
                towedVehicle = vehicle
            end
        end

        local detachCommand = "detachTowBar"
        if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.performDetachTowBar == nil then
            detachCommand = "detachConstraint"
        end

        local args = {}
        if towingVehicle ~= nil then
            args.towingVehicle = towingVehicle:getId()
        end
        if towedVehicle ~= nil then
            args.vehicle = towedVehicle:getId()
        else
            args.vehicle = vehicle:getId()
        end

        sendClientCommand(playerObj, "towbar", detachCommand, args)
        return true
    end
    return false
end

local function towingLoopDetected(startVehicle)
    if startVehicle == nil then return false end
    local visited = {}
    local vehicle = startVehicle
    while vehicle ~= nil do
        local id = vehicle:getId()
        if visited[id] then
            return true
        end
        visited[id] = true
        vehicle = vehicle:getVehicleTowing()
    end
    return false
end

local function sanitizeLoadedTowLinks()
    local vehicles = getLoadedVehicles()
    if #vehicles == 0 then return vehicles end

    for _, vehicle in ipairs(vehicles) do
        local front = vehicle:getVehicleTowedBy()
        local rear = vehicle:getVehicleTowing()

        if front == vehicle or rear == vehicle then
            breakConstraintSafe(vehicle, "self-link")
        elseif towingLoopDetected(vehicle) then
            breakConstraintSafe(vehicle, "loop")
        end
    end

    return vehicles
end

local function setModDataNumberIfChanged(modData, key, value)
    if modData == nil or key == nil or value == nil then return false end
    local current = tonumber(modData[key])
    if current ~= nil and math.abs(current - value) < 0.0001 then
        return false
    end
    modData[key] = value
    return true
end

local function saveLandtrainVehiclePosition(vehicle)
    if vehicle == nil then return false end
    local modData = vehicle:getModData()
    if modData == nil then return false end

    local changed = false
    changed = setModDataNumberIfChanged(modData, LANDTRAIN_SAVED_POS_X_KEY, vehicle:getX()) or changed
    changed = setModDataNumberIfChanged(modData, LANDTRAIN_SAVED_POS_Y_KEY, vehicle:getY()) or changed
    changed = setModDataNumberIfChanged(modData, LANDTRAIN_SAVED_POS_Z_KEY, vehicle:getZ()) or changed
    if vehicle.getDirectionAngle then
        local ok, angle = pcall(function()
            return vehicle:getDirectionAngle()
        end)
        if ok and angle ~= nil then
            changed = setModDataNumberIfChanged(modData, LANDTRAIN_SAVED_DIR_KEY, angle) or changed
        end
    end

    if changed then
        vehicle:transmitModData()
    end
    return changed
end

local function isLandtrainTowbarLink(frontVehicle, rearVehicle)
    if frontVehicle == nil or rearVehicle == nil then return false end
    if frontVehicle == rearVehicle or frontVehicle:getId() == rearVehicle:getId() then return false end
    if frontVehicle:getVehicleTowing() ~= rearVehicle or rearVehicle:getVehicleTowedBy() ~= frontVehicle then
        return false
    end

    local frontModData = frontVehicle:getModData()
    local rearModData = rearVehicle:getModData()
    if frontModData == nil or rearModData == nil then return false end
    return isTowbarManagedVehicle(frontVehicle)
        or isTowbarManagedVehicle(rearVehicle)
        or tonumber(rearModData[LANDTRAIN_FRONT_SQL_ID_KEY]) ~= nil
        or tonumber(frontModData[LANDTRAIN_REAR_SQL_ID_KEY]) ~= nil
end

local function saveLandtrainTowbarLink(frontVehicle, rearVehicle, attachmentA, attachmentB)
    if not isLandtrainTowbarLink(frontVehicle, rearVehicle) then return false end
    storeLandtrainFrontLinkData(frontVehicle, rearVehicle, attachmentA, attachmentB)
    local changed = false
    changed = saveLandtrainVehiclePosition(frontVehicle) or changed
    changed = saveLandtrainVehiclePosition(rearVehicle) or changed
    return changed
end

local function saveActiveLandtrainTowbarSnapshot(vehicles)
    local loaded = vehicles or getLoadedVehicles()
    local savedLinks = 0
    for _, rearVehicle in ipairs(loaded) do
        local frontVehicle = rearVehicle:getVehicleTowedBy()
        if isLandtrainTowbarLink(frontVehicle, rearVehicle) then
            local rearModData = rearVehicle:getModData()
            local attachmentA, attachmentB = chooseLandtrainTowbarPair(
                frontVehicle,
                rearVehicle,
                rearModData and rearModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] or nil,
                rearModData and rearModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] or nil
            )
            if saveLandtrainTowbarLink(frontVehicle, rearVehicle, attachmentA, attachmentB) then
                savedLinks = savedLinks + 1
            end
        end
    end
    return savedLinks
end

local function callVehicleMethodSafe(vehicle, methodName, ...)
    if vehicle == nil then return false end
    local method = vehicle[methodName]
    if type(method) ~= "function" then return false end
    local ok = pcall(method, vehicle, ...)
    return ok
end

local function restoreSavedVehiclePosition(vehicle)
    if vehicle == nil then return false end
    local modData = vehicle:getModData()
    if modData == nil then return false end

    local x = tonumber(modData[LANDTRAIN_SAVED_POS_X_KEY])
    local y = tonumber(modData[LANDTRAIN_SAVED_POS_Y_KEY])
    local z = tonumber(modData[LANDTRAIN_SAVED_POS_Z_KEY])
    if x == nil or y == nil or z == nil then
        return false
    end

    local moved = false
    moved = callVehicleMethodSafe(vehicle, "setX", x) or moved
    moved = callVehicleMethodSafe(vehicle, "setY", y) or moved
    moved = callVehicleMethodSafe(vehicle, "setZ", z) or moved
    callVehicleMethodSafe(vehicle, "setLx", x)
    callVehicleMethodSafe(vehicle, "setLy", y)
    callVehicleMethodSafe(vehicle, "setLz", z)

    local dir = tonumber(modData[LANDTRAIN_SAVED_DIR_KEY])
    if dir ~= nil then
        callVehicleMethodSafe(vehicle, "setDirectionAngle", dir)
    end

    local cell = getCell()
    if cell ~= nil then
        local square = cell:getGridSquare(math.floor(x), math.floor(y), math.floor(z))
        if square ~= nil then
            callVehicleMethodSafe(vehicle, "setCurrent", square)
        end
    end

    callVehicleMethodSafe(vehicle, "transmitPosition")
    return moved
end

local function collectSavedLandtrainLinks(vehicles)
    local links = {}
    local seenRear = {}
    local frontSqlByRearSql = {}
    for _, vehicle in ipairs(vehicles) do
        local modData = vehicle and vehicle:getModData() or nil
        local rearSqlId = vehicle and vehicle.getSqlId and vehicle:getSqlId() or nil
        local frontSqlId = modData and tonumber(modData[LANDTRAIN_FRONT_SQL_ID_KEY]) or nil
        if rearSqlId ~= nil and rearSqlId >= 0 and frontSqlId ~= nil and frontSqlId >= 0 and frontSqlId ~= rearSqlId then
            frontSqlByRearSql[rearSqlId] = frontSqlId
        end
    end

    local function getSavedDepthForRearSql(rearSqlId)
        if rearSqlId == nil or rearSqlId < 0 then return 0 end
        local depth = 0
        local seen = {}
        local cursorSqlId = rearSqlId
        while cursorSqlId ~= nil do
            local frontSqlId = frontSqlByRearSql[cursorSqlId]
            if frontSqlId == nil or frontSqlId < 0 then
                break
            end
            if seen[frontSqlId] then
                break
            end
            seen[frontSqlId] = true
            depth = depth + 1
            cursorSqlId = frontSqlId
        end
        return depth
    end

    for _, rearVehicle in ipairs(vehicles) do
        if rearVehicle ~= nil then
            local rearId = rearVehicle:getId()
            if not seenRear[rearId] then
                local rearModData = rearVehicle:getModData()
                local frontSqlId = rearModData and tonumber(rearModData[LANDTRAIN_FRONT_SQL_ID_KEY]) or nil
                local frontVehicle = nil
                if frontSqlId ~= nil and frontSqlId >= 0 then
                    frontVehicle = getVehicleBySqlIdSafe(frontSqlId)
                end
                if frontVehicle == nil then
                    frontVehicle = rearVehicle:getVehicleTowedBy()
                end
                if frontVehicle ~= nil and frontVehicle ~= rearVehicle and frontVehicle:getId() ~= rearVehicle:getId() then
                    local attachmentA, attachmentB = chooseLandtrainTowbarPair(
                        frontVehicle,
                        rearVehicle,
                        rearModData and rearModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] or nil,
                        rearModData and rearModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] or nil
                    )
                    table.insert(links, {
                        frontVehicle = frontVehicle,
                        rearVehicle = rearVehicle,
                        attachmentA = attachmentA,
                        attachmentB = attachmentB,
                        depth = getSavedDepthForRearSql(rearVehicle.getSqlId and rearVehicle:getSqlId() or nil)
                    })
                    seenRear[rearId] = true
                end
            end
        end
    end
    table.sort(links, function(a, b)
        if a.depth == b.depth then
            return a.rearVehicle:getId() < b.rearVehicle:getId()
        end
        return a.depth < b.depth
    end)
    return links
end

local function detachSavedLandtrainLinks(links)
    local detached = 0
    for i = #links, 1, -1 do
        local link = links[i]
        local frontVehicle = link.frontVehicle
        local rearVehicle = link.rearVehicle
        if frontVehicle ~= nil and rearVehicle ~= nil
            and (frontVehicle:getVehicleTowing() == rearVehicle or rearVehicle:getVehicleTowedBy() == frontVehicle) then
            if breakConstraintSafe(frontVehicle, "load-rebuild-detach") then
                detached = detached + 1
            end
        end
    end
    return detached
end

local function attachSavedLandtrainLink(playerObj, link, delayTicks)
    local frontVehicle = link.frontVehicle
    local rearVehicle = link.rearVehicle
    if playerObj == nil or frontVehicle == nil or rearVehicle == nil then return false end
    if TowBarMod == nil or TowBarMod.Utils == nil or TowBarMod.Utils.updateAttachmentsForRigidTow == nil then
        return false
    end
    if frontVehicle == rearVehicle or frontVehicle:getId() == rearVehicle:getId() then
        return false
    end

    local scriptA = frontVehicle:getScript()
    local scriptB = rearVehicle:getScript()
    if scriptA == nil or scriptB == nil then
        return false
    end
    if scriptA:getAttachmentById(link.attachmentA) == nil or scriptB:getAttachmentById(link.attachmentB) == nil then
        return false
    end

    local originalScriptName = rearVehicle:getScriptName()
    updateAttachmentsForRigidTowNoStack(frontVehicle, rearVehicle, link.attachmentA, link.attachmentB)
    rearVehicle:setScriptName("notTowingA_Trailer")
    local args = {
        vehicleA = frontVehicle:getId(),
        vehicleB = rearVehicle:getId(),
        attachmentA = link.attachmentA,
        attachmentB = link.attachmentB
    }
    if args.vehicleA == args.vehicleB then
        return false
    end
    sendClientCommand(playerObj, "vehicle", "attachTrailer", args)

    local frontModData = frontVehicle:getModData()
    local rearModData = rearVehicle:getModData()
    if frontModData == nil or rearModData == nil then return false end
    frontModData["isTowingByTowBar"] = true
    rearModData["isTowingByTowBar"] = true
    rearModData["towed"] = true
    if rearModData.towBarOriginalScriptName == nil and originalScriptName ~= "notTowingA_Trailer" then
        rearModData.towBarOriginalScriptName = originalScriptName
    end
    if rearModData.towBarOriginalMass == nil then
        rearModData.towBarOriginalMass = rearVehicle:getMass()
    end
    if rearModData.towBarOriginalBrakingForce == nil then
        rearModData.towBarOriginalBrakingForce = rearVehicle:getBrakingForce()
    end
    frontVehicle:transmitModData()
    rearVehicle:transmitModData()
    storeLandtrainFrontLinkData(frontVehicle, rearVehicle, link.attachmentA, link.attachmentB)

    if TowBarScheduleAction and TowBarMod and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
        ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, delayTicks, TowBarMod.Hook.setVehiclePostAttach, rearVehicle))
    elseif TowBarMod and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
        TowBarMod.Hook.setVehiclePostAttach(playerObj, rearVehicle)
    end
    if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.markReapplied then
        TowBarMod.Hook.markReapplied(rearVehicle)
    end

    setTowBarModelVisibleForVehicle(rearVehicle, true)
    refreshTowBarState(frontVehicle)
    refreshTowBarState(rearVehicle)
    return true
end

local function runLandtrainSingleLoadRestorePass()
    local vehicles = sanitizeLoadedTowLinks()
    if vehicles == nil or #vehicles == 0 then
        ltInfo("Landtrain load rebuild: no loaded vehicles")
        return
    end

    local links = collectSavedLandtrainLinks(vehicles)
    if #links == 0 then
        ltInfo("Landtrain load rebuild: no saved towbar links")
        return
    end

    local playerObj = getPlayer() or getSpecificPlayer(0)
    if playerObj == nil then
        ltInfo("Landtrain load rebuild skipped: no player")
        return
    end
    if TowBarMod == nil or TowBarMod.Utils == nil or TowBarMod.Utils.updateAttachmentsForRigidTow == nil then
        ltInfo("Landtrain load rebuild skipped: TowBar utilities unavailable")
        return
    end

    local movedVehicles = 0
    local movedSeen = {}
    for _, link in ipairs(links) do
        local frontId = link.frontVehicle:getId()
        local rearId = link.rearVehicle:getId()
        if not movedSeen[frontId] then
            if restoreSavedVehiclePosition(link.frontVehicle) then
                movedVehicles = movedVehicles + 1
            end
            movedSeen[frontId] = true
        end
        if not movedSeen[rearId] then
            if restoreSavedVehiclePosition(link.rearVehicle) then
                movedVehicles = movedVehicles + 1
            end
            movedSeen[rearId] = true
        end
    end

    local detachedCount = detachSavedLandtrainLinks(links)
    local attachedCount = 0
    for index, link in ipairs(links) do
        local delay = 10 + ((index - 1) * 5)
        if attachSavedLandtrainLink(playerObj, link, delay) then
            attachedCount = attachedCount + 1
        end
    end

    saveActiveLandtrainTowbarSnapshot(vehicles)
    ltInfo("Landtrain load rebuild complete moved=" .. tostring(movedVehicles)
        .. " detached=" .. tostring(detachedCount)
        .. " attached=" .. tostring(attachedCount)
        .. " links=" .. tostring(#links))
end

local landtrainLoadRebuildDone = false
local landtrainSaveSnapshotTickCounter = 0
local function onLandtrainSaveSnapshotTick()
    if not landtrainLoadRebuildDone then
        return
    end
    landtrainSaveSnapshotTickCounter = landtrainSaveSnapshotTickCounter + 1
    if landtrainSaveSnapshotTickCounter < 60 then
        return
    end
    landtrainSaveSnapshotTickCounter = 0
    saveActiveLandtrainTowbarSnapshot()
end

local landtrainLoadRestoreDelayTicks = 0
local function onLandtrainSingleLoadRestoreTick()
    if landtrainLoadRestoreDelayTicks > 0 then
        landtrainLoadRestoreDelayTicks = landtrainLoadRestoreDelayTicks - 1
        return
    end
    Events.OnTick.Remove(onLandtrainSingleLoadRestoreTick)
    runLandtrainSingleLoadRestorePass()
    landtrainLoadRebuildDone = true
end

local function scheduleLandtrainSingleLoadRestore()
    -- Single delayed load pass: restore saved positions, detach all saved links, reattach in chain order.
    landtrainLoadRebuildDone = false
    landtrainSaveSnapshotTickCounter = 0
    landtrainLoadRestoreDelayTicks = 90
    Events.OnTick.Remove(onLandtrainSingleLoadRestoreTick)
    Events.OnTick.Add(onLandtrainSingleLoadRestoreTick)
end

local _landtrainHookPosA = Vector3f.new()
local _landtrainHookPosB = Vector3f.new()

local function hasTowAttachment(vehicle, attachmentId)
    if vehicle == nil or attachmentId == nil then return false end
    local script = vehicle:getScript()
    if script == nil then return false end
    return script:getAttachmentById(attachmentId) ~= nil
end

local function isAttachmentSideFree(vehicle, attachmentId)
    if vehicle == nil then return false end
    if attachmentId == "trailer" then
        return vehicle:getVehicleTowing() == nil
    end
    if attachmentId == "trailerfront" then
        return vehicle:getVehicleTowedBy() == nil
    end
    return true
end

local function canTowByLandtrain(vehicleA, vehicleB, attachmentA, attachmentB)
    if vehicleA == nil or vehicleB == nil then return false end
    if vehicleA == vehicleB then return false end
    if not hasTowAttachment(vehicleA, attachmentA) then return false end
    if not hasTowAttachment(vehicleB, attachmentB) then return false end
    if not isAttachmentSideFree(vehicleA, attachmentA) then return false end
    if not isAttachmentSideFree(vehicleB, attachmentB) then return false end
    if vehicleA:getVehicleTowing() == vehicleB or vehicleA:getVehicleTowedBy() == vehicleB then
        return false
    end

    -- Keep vanilla behavior when possible.
    if vehicleA:canAttachTrailer(vehicleB, attachmentA, attachmentB) then
        ltLog("canTowByLandtrain vanilla=true A=" .. vehLabel(vehicleA) .. " B=" .. vehLabel(vehicleB) .. " attA=" .. tostring(attachmentA) .. " attB=" .. tostring(attachmentB))
        return true
    end

    -- Keep first hookup identical to Towbar/vanilla distance checks.
    local vehicleALinked = (vehicleA:getVehicleTowing() ~= nil or vehicleA:getVehicleTowedBy() ~= nil)
    local vehicleBLinked = (vehicleB:getVehicleTowing() ~= nil or vehicleB:getVehicleTowedBy() ~= nil)
    if not vehicleALinked and not vehicleBLinked then
        ltLog("canTowByLandtrain fallback=false (unlinked pair) A=" .. vehLabel(vehicleA) .. " B=" .. vehLabel(vehicleB))
        return false
    end

    -- Vanilla blocks chained towing here; allow only near-identical close-range hookups.
    local posA = vehicleA:getAttachmentWorldPos(attachmentA, _landtrainHookPosA)
    local posB = vehicleB:getAttachmentWorldPos(attachmentB, _landtrainHookPosB)
    if posA == nil or posB == nil then return false end

    local dx = posA:x() - posB:x()
    local dy = posA:y() - posB:y()
    local dz = posA:z() - posB:z()
    local distSq = dx * dx + dy * dy + dz * dz
    local allow = distSq <= LANDTRAIN_FALLBACK_MAX_DIST_SQ and math.abs(dz) <= LANDTRAIN_FALLBACK_MAX_DZ
    ltLog("canTowByLandtrain fallback=" .. tostring(allow)
        .. " A=" .. vehLabel(vehicleA)
        .. " B=" .. vehLabel(vehicleB)
        .. " attA=" .. tostring(attachmentA)
        .. " attB=" .. tostring(attachmentB)
        .. " distSq=" .. string.format("%.3f", distSq)
        .. " dz=" .. string.format("%.3f", dz))
    return allow
end

local function getAttachLabel(vehicleA, vehicleB)
    return getText("UI_Text_Towing_byTowBar")
end

local function captureTowbarFrontLink(towingVehicle)
    if towingVehicle == nil then
        ltLog("captureTowbarFrontLink towingVehicle=nil")
        return nil
    end

    local frontVehicle = towingVehicle:getVehicleTowedBy()
    if frontVehicle == nil then
        ltLog("captureTowbarFrontLink no front link for " .. vehLabel(towingVehicle))
        return nil
    end
    if frontVehicle == towingVehicle then
        ltLog("captureTowbarFrontLink invalid self front for " .. vehLabel(towingVehicle))
        return nil
    end
    if towingVehicle:getVehicleTowing() == frontVehicle then
        ltLog("captureTowbarFrontLink invalid two-way loop front=" .. vehLabel(frontVehicle) .. " middle=" .. vehLabel(towingVehicle))
        return nil
    end
    local frontRear = frontVehicle:getVehicleTowing()
    if frontRear ~= nil and frontRear ~= towingVehicle then
        ltLog("captureTowbarFrontLink invalid front rear mismatch front=" .. vehLabel(frontVehicle) .. " rear=" .. vehLabel(frontRear) .. " middle=" .. vehLabel(towingVehicle))
        return nil
    end

    local towingModData = towingVehicle:getModData()
    local savedAttachmentA = towingModData and towingModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] or nil
    local savedAttachmentB = towingModData and towingModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] or nil
    local attachmentA, attachmentB = chooseLandtrainTowbarPair(frontVehicle, towingVehicle, savedAttachmentA, savedAttachmentB)
    local link = {
        frontVehicle = frontVehicle,
        towingVehicle = towingVehicle,
        attachmentA = attachmentA,
        attachmentB = attachmentB
    }
    ltLog("captureTowbarFrontLink captured front=" .. vehLabel(frontVehicle) .. " middle=" .. vehLabel(towingVehicle)
        .. " attA=" .. tostring(link.attachmentA) .. " attB=" .. tostring(link.attachmentB))
    return link
end

local function restoreTowbarFrontLink(playerObj, link)
    if link == nil then
        ltLog("restoreTowbarFrontLink skipped (no captured link)")
        return
    end
    local frontVehicle = link.frontVehicle
    local towingVehicle = link.towingVehicle
    if frontVehicle == nil or towingVehicle == nil then
        ltLog("restoreTowbarFrontLink invalid captured refs")
        return
    end
    if frontVehicle == towingVehicle or frontVehicle:getId() == towingVehicle:getId() then
        ltLog("restoreTowbarFrontLink rejected self-link for " .. vehLabel(towingVehicle))
        clearLandtrainFrontLinkData(towingVehicle)
        return
    end
    if towingVehicle:getVehicleTowedBy() ~= nil then
        ltLog("restoreTowbarFrontLink not needed; front still connected for middle=" .. vehLabel(towingVehicle))
        return
    end

    ltLog("restoreTowbarFrontLink restoring front=" .. vehLabel(frontVehicle) .. " middle=" .. vehLabel(towingVehicle))
    updateAttachmentsForRigidTowNoStack(frontVehicle, towingVehicle, link.attachmentA, link.attachmentB)
    towingVehicle:setScriptName("notTowingA_Trailer")

    local args = {
        vehicleA = frontVehicle:getId(),
        vehicleB = towingVehicle:getId(),
        attachmentA = link.attachmentA,
        attachmentB = link.attachmentB
    }
    if args.vehicleA == args.vehicleB then
        ltLog("restoreTowbarFrontLink blocked self attach args for " .. vehLabel(towingVehicle))
        clearLandtrainFrontLinkData(towingVehicle)
        return
    end
    sendClientCommand(playerObj, "vehicle", "attachTrailer", args)
    ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towingVehicle))

    local frontModData = frontVehicle:getModData()
    local towingModData = towingVehicle:getModData()
    frontModData["isTowingByTowBar"] = true
    towingModData["isTowingByTowBar"] = true
    towingModData["towed"] = true
    storeLandtrainFrontLinkData(frontVehicle, towingVehicle, link.attachmentA, link.attachmentB)
    frontVehicle:transmitModData()
    towingVehicle:transmitModData()
    dumpTowState("restoreTowbarFrontLink after front", frontVehicle)
    dumpTowState("restoreTowbarFrontLink after middle", towingVehicle)
end

local function queueTowbarFrontLinkRestore(playerObj, link, delayTicks)
    if link == nil or playerObj == nil then return end
    local delay = delayTicks or 15
    ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, delay, function(character, linkArg)
        ltLog("queueTowbarFrontLinkRestore fire delay=" .. tostring(delay) .. " middle=" .. vehLabel(linkArg and linkArg.towingVehicle or nil))
        restoreTowbarFrontLink(character, linkArg)
    end, link))
end

local function ensureTowAttachmentsForTrailers()
    local scriptManager = getScriptManager()
    if scriptManager == nil then return end

    local vehicleScripts = scriptManager:getAllVehicleScripts()
    if vehicleScripts == nil then return end

    for i = 0, vehicleScripts:size() - 1 do
        local script = vehicleScripts:get(i)
        local scriptName = script and script:getName() or nil
        if script and scriptName and string.match(string.lower(scriptName), "trailer") then
            local wheelCount = script:getWheelCount()
            local attachHeightOffset = -0.5
            if wheelCount > 0 then
                attachHeightOffset = script:getWheel(0):getOffset():y() + 0.1
            end

            local rearTow = script:getAttachmentById("trailer")
            if rearTow == nil then
                local attach = ModelAttachment.new("trailer")
                attach:getOffset():set(0, attachHeightOffset, -script:getPhysicsChassisShape():z() / 2 - 0.1)
                attach:setZOffset(-1)
                script:addAttachment(attach)
            end

            local frontTow = script:getAttachmentById("trailerfront")
            if frontTow == nil then
                local attach = ModelAttachment.new("trailerfront")
                attach:getOffset():set(0, attachHeightOffset, script:getPhysicsChassisShape():z() / 2 + 0.1)
                attach:setZOffset(1)
                script:addAttachment(attach)
            end
        end
    end
end

local function captureTowbarAttachmentDefaults()
    local scriptManager = getScriptManager()
    if scriptManager == nil then return end

    local vehicleScripts = scriptManager:getAllVehicleScripts()
    if vehicleScripts == nil then return end

    for i = 0, vehicleScripts:size() - 1 do
        local script = vehicleScripts:get(i)
        if script ~= nil then
            cacheAttachmentBaseOffset(script, "trailer")
            cacheAttachmentBaseOffset(script, "trailerfront")
        end
    end
end

local function menuHasTowbarAttachSlice(menu)
    if menu == nil or menu.slices == nil then return false end
    local attachAction = TowBarMod and TowBarMod.Hook and TowBarMod.Hook.attachByTowBarAction or nil
    local chooseAction = TowBarMod and TowBarMod.UI and TowBarMod.UI.showChooseVehicleMenu or nil
    if attachAction == nil and chooseAction == nil then
        return false
    end

    for _, slice in ipairs(menu.slices) do
        local command = slice.command and slice.command[1]
        if (attachAction ~= nil and command == attachAction)
            or (chooseAction ~= nil and command == chooseAction) then
            return true
        end
    end
    return false
end

local function getTowbarDetachAction()
    if TowBarMod == nil or TowBarMod.Hook == nil then return nil end
    return TowBarMod.Hook.deattachTowBarAction or TowBarMod.Hook.detachTowBarAction
end

local function getTowbarPerformDetachHook()
    if TowBarMod == nil or TowBarMod.Hook == nil then return nil end
    return TowBarMod.Hook.performDetachTowBar or TowBarMod.Hook.performDeattachTowBar
end

local function menuHasTowbarDetachSlice(menu)
    if menu == nil or menu.slices == nil then return false end
    local detachAction = getTowbarDetachAction()
    if detachAction == nil then
        return false
    end
    for _, slice in ipairs(menu.slices) do
        local command = slice.command and slice.command[1]
        if command == detachAction then
            return true
        end
    end
    return false
end

local function hasTowbarPart(vehicle)
    if vehicle == nil then return false end
    return vehicle:getPartById("towbar") ~= nil
end

local function getDetachPairFromVehicle(vehicle)
    if vehicle == nil then return nil, nil end
    if vehicle:getVehicleTowedBy() ~= nil then
        return vehicle:getVehicleTowedBy(), vehicle
    end
    if vehicle:getVehicleTowing() ~= nil then
        return vehicle, vehicle:getVehicleTowing()
    end
    return nil, nil
end

local function isLikelyTowbarDetachPair(towingVehicle, towedVehicle)
    if towingVehicle == nil or towedVehicle == nil then return false end

    local towingModData = towingVehicle:getModData()
    local towedModData = towedVehicle:getModData()
    if towingModData ~= nil then
        if towingModData["isTowingByTowBar"] == true or towingModData["towed"] == true then
            return true
        end
    end
    if towedModData ~= nil then
        if towedModData["isTowingByTowBar"] == true
            or towedModData["towed"] == true
            or towedModData.towBarOriginalScriptName ~= nil
            or towedModData.towBarOriginalMass ~= nil
            or towedModData.towBarOriginalBrakingForce ~= nil
            or towedModData["isChangedTowedAttachment"] == true
            or towedModData["towBarChangedAttachmentId"] ~= nil then
            return true
        end
    end

    return hasTowbarPart(towingVehicle) and hasTowbarPart(towedVehicle)
end

local function getTowbarDetachTargetVehicle(vehicle)
    local towingVehicle, towedVehicle = getDetachPairFromVehicle(vehicle)
    if isLikelyTowbarDetachPair(towingVehicle, towedVehicle) then
        return towedVehicle
    end
    return nil
end

local function isTowbarChainVehicleOrNeighbor(vehicle)
    if vehicle == nil then return false end
    if getTowbarDetachTargetVehicle(vehicle) ~= nil then return true end
    if isTowbarManagedVehicle(vehicle) then return true end

    local front = vehicle:getVehicleTowedBy()
    local rear = vehicle:getVehicleTowing()
    if getTowbarDetachTargetVehicle(front) ~= nil then return true end
    if getTowbarDetachTargetVehicle(rear) ~= nil then return true end
    if isTowbarManagedVehicle(front) then return true end
    if isTowbarManagedVehicle(rear) then return true end
    return false
end

local function addLandtrainUnhookOptionToMenu(playerObj, vehicle)
    if playerObj == nil or vehicle == nil then return end
    local detachAction = getTowbarDetachAction()
    if detachAction == nil then return end

    local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
    if menu == nil then return end
    if menuHasTowbarDetachSlice(menu) then return end

    local towedVehicle = getTowbarDetachTargetVehicle(vehicle)
    if towedVehicle == nil then return end

    menu:addSlice(
        getText("ContextMenu_Vehicle_DetachTrailer", ISVehicleMenu.getVehicleDisplayName(towedVehicle)),
        getTexture("media/textures/tow_bar_detach.png"),
        detachAction,
        playerObj,
        towedVehicle
    )
end

local function getLandtrainHookTypeVariants(vehicleA, vehicleB)
    local hookTypeVariants = {}
    if vehicleA == nil or vehicleB == nil or vehicleA == vehicleB then
        return hookTypeVariants
    end

    if canTowByLandtrain(vehicleA, vehicleB, "trailerfront", "trailer") then
        local hookType = {}
        hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getAttachLabel(vehicleA, vehicleB)
        hookType.func = TowBarMod.Hook.attachByTowBarAction
        hookType.towingVehicle = vehicleB
        hookType.towedVehicle = vehicleA
        hookType.textureName = "tow_bar_icon"
        table.insert(hookTypeVariants, hookType)
    elseif canTowByLandtrain(vehicleA, vehicleB, "trailer", "trailerfront") then
        local hookType = {}
        hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getAttachLabel(vehicleA, vehicleB)
        hookType.func = TowBarMod.Hook.attachByTowBarAction
        hookType.towingVehicle = vehicleA
        hookType.towedVehicle = vehicleB
        hookType.textureName = "tow_bar_icon"
        table.insert(hookTypeVariants, hookType)
    end

    ltLog("getLandtrainHookTypeVariants A=" .. vehLabel(vehicleA) .. " B=" .. vehLabel(vehicleB) .. " count=" .. tostring(#hookTypeVariants))
    return hookTypeVariants
end

local function getNearbyLandtrainTargets(mainVehicle)
    local vehicles = {}
    local square = mainVehicle and mainVehicle:getSquare() or nil
    if square == nil then
        ltLog("getNearbyLandtrainTargets no square for " .. vehLabel(mainVehicle))
        return vehicles
    end

    for y = square:getY() - 6, square:getY() + 6 do
        for x = square:getX() - 6, square:getX() + 6 do
            local square2 = getCell():getGridSquare(x, y, square:getZ())
            if square2 then
                for i = 1, square2:getMovingObjects():size() do
                    local obj = square2:getMovingObjects():get(i - 1)
                    if obj ~= nil and instanceof(obj, "BaseVehicle") and obj ~= mainVehicle then
                        local variants = getLandtrainHookTypeVariants(mainVehicle, obj)
                        if #variants > 0 then
                            table.insert(vehicles, { vehicle = obj, variants = variants })
                            ltLog("getNearbyLandtrainTargets candidate main=" .. vehLabel(mainVehicle) .. " target=" .. vehLabel(obj) .. " variants=" .. tostring(#variants))
                        end
                    end
                end
            end
        end
    end
    ltLog("getNearbyLandtrainTargets total main=" .. vehLabel(mainVehicle) .. " targets=" .. tostring(#vehicles))
    return vehicles
end

local function addLandtrainHookOptionToMenu(playerObj, vehicle)
    if playerObj == nil or vehicle == nil then return end
    local hasTowBarItem = false
    if playerObj.getInventory and playerObj:getInventory() ~= nil then
        hasTowBarItem = playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar") ~= nil
    end
    if not hasTowBarItem and TowBarMod and TowBarMod.Hook and TowBarMod.Hook.getTowBarInventoryItem then
        hasTowBarItem = TowBarMod.Hook.getTowBarInventoryItem(playerObj) ~= nil
    end
    if not hasTowBarItem and TowBarMod and TowBarMod.Utils and TowBarMod.Utils.getTowBarInventoryItem then
        hasTowBarItem = TowBarMod.Utils.getTowBarInventoryItem(playerObj) ~= nil
    end

    if not hasTowBarItem then
        ltLog("addLandtrainHookOptionToMenu no TowBar item for " .. vehLabel(vehicle))
        return
    end
    ltLog("addLandtrainHookOptionToMenu vehicle=" .. vehLabel(vehicle))

    local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
    if menu == nil then return end

    local targets = getNearbyLandtrainTargets(vehicle)
    if #targets == 0 then
        ltLog("addLandtrainHookOptionToMenu no nearby valid targets for " .. vehLabel(vehicle))
        return
    end

    if #targets == 1 then
        local hookType = targets[1].variants[1]
        menu:addSlice(
            hookType.name,
            getTexture("media/textures/tow_bar_attach.png"),
            hookType.func,
            playerObj,
            hookType.towingVehicle,
            hookType.towedVehicle,
            hookType.towingPoint,
            hookType.towedPoint
        )
        return
    end

    -- Reuse Towbar's chooser UI by passing only the candidate vehicles.
    local vehicleList = {}
    for _, entry in ipairs(targets) do
        table.insert(vehicleList, entry.vehicle)
    end
    menu:addSlice(
        getText("UI_Text_Towing_attach") .. "...",
        getTexture("media/textures/tow_bar_attach.png"),
        TowBarMod.UI.showChooseVehicleMenu,
        playerObj,
        vehicle,
        vehicleList,
        true
    )
    ltLog("addLandtrainHookOptionToMenu added chooser with " .. tostring(#vehicleList) .. " targets for " .. vehLabel(vehicle))
end

local function installLandtrainTowbarPatch()
    if not TowBarMod or not TowBarMod.Utils or not TowBarMod.UI or not TowBarMod.Hook then
        ltLog("installLandtrainTowbarPatch waiting for Towbar globals")
        return false
    end

    -- Override Towbar's single-link limitation so a vehicle can be part of a chain.
    if TowBarMod.Utils.getHookTypeVariants ~= TowBarMod.Utils._landtrainGetHookVariantsWrapper then
        local hookVariantsWrapper = function(vehicleA, vehicleB, hasTowBar)
            local hookTypeVariants = {}
            if not hasTowBar then return hookTypeVariants end
            if vehicleA == nil or vehicleB == nil then return hookTypeVariants end
            if vehicleA == vehicleB then return hookTypeVariants end

            -- Allow trailer <-> vehicle and trailer <-> trailer links for landtrains.
            if canTowByLandtrain(vehicleA, vehicleB, "trailerfront", "trailer") then
                local hookType = {}
                hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getAttachLabel(vehicleA, vehicleB)
                hookType.func = TowBarMod.Hook.attachByTowBarAction
                hookType.towingVehicle = vehicleB
                hookType.towedVehicle = vehicleA
                hookType.textureName = "tow_bar_icon"
                table.insert(hookTypeVariants, hookType)
            elseif canTowByLandtrain(vehicleA, vehicleB, "trailer", "trailerfront") then
                local hookType = {}
                hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getAttachLabel(vehicleA, vehicleB)
                hookType.func = TowBarMod.Hook.attachByTowBarAction
                hookType.towingVehicle = vehicleA
                hookType.towedVehicle = vehicleB
                hookType.textureName = "tow_bar_icon"
                table.insert(hookTypeVariants, hookType)
            end

            return hookTypeVariants
        end

        TowBarMod.Utils.getHookTypeVariants = hookVariantsWrapper
        TowBarMod.Utils._landtrainGetHookVariantsWrapper = hookVariantsWrapper
        ltLog("installLandtrainTowbarPatch patched TowBarMod.Utils.getHookTypeVariants")
    end

    -- Keep towbar state valid for middle links in a chain after detach/attach.
    local originalPerformAttach = TowBarMod.Hook.performAttachTowBar
    if originalPerformAttach and originalPerformAttach ~= TowBarMod.Hook._landtrainPerformAttachWrapper then
        local performAttachWrapper = function(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)
            if towingVehicle == nil or towedVehicle == nil then return end
            if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then
                ltLog("performAttachTowBar blocked self-link args towing=" .. vehLabel(towingVehicle))
                clearLandtrainFrontLinkData(towedVehicle)
                return
            end
            ltLog("performAttachTowBar begin towing=" .. vehLabel(towingVehicle) .. " towed=" .. vehLabel(towedVehicle)
                .. " attA=" .. tostring(attachmentA) .. " attB=" .. tostring(attachmentB))
            dumpTowState("performAttachTowBar pre towing", towingVehicle)
            dumpTowState("performAttachTowBar pre towed", towedVehicle)
            local frontLink = captureTowbarFrontLink(towingVehicle)
            originalPerformAttach(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)
            dumpTowState("performAttachTowBar post-original towing", towingVehicle)
            dumpTowState("performAttachTowBar post-original towed", towedVehicle)
            storeLandtrainFrontLinkData(towingVehicle, towedVehicle, attachmentA, attachmentB)
            restoreTowbarFrontLink(playerObj, frontLink)
            queueTowbarFrontLinkRestore(playerObj, frontLink, 12)
            queueTowbarFrontLinkRestore(playerObj, frontLink, 30)
            dumpTowState("performAttachTowBar post-restore towing", towingVehicle)
            dumpTowState("performAttachTowBar post-restore towed", towedVehicle)

            setTowBarModelVisibleForVehicle(towedVehicle, true)
            refreshTowBarState(towingVehicle)
            refreshTowBarState(towedVehicle)
            if towingVehicle then
                refreshTowBarState(towingVehicle:getVehicleTowedBy())
            end
            if towedVehicle then
                refreshTowBarState(towedVehicle:getVehicleTowing())
            end
            saveActiveLandtrainTowbarSnapshot()
        end

        TowBarMod.Hook.performAttachTowBar = performAttachWrapper
        TowBarMod.Hook._landtrainPerformAttachWrapper = performAttachWrapper
        ltLog("installLandtrainTowbarPatch patched TowBarMod.Hook.performAttachTowBar")
    end

    local function resolveTowbarDetachPair(towingVehicle, towedVehicle)
        local resolvedTowing = towingVehicle
        local resolvedTowed = nil

        if resolvedTowing ~= nil then
            resolvedTowed = resolvedTowing:getVehicleTowing()
        end

        if resolvedTowed == nil and towedVehicle ~= nil and towedVehicle:getVehicleTowedBy() ~= nil then
            resolvedTowing = towedVehicle:getVehicleTowedBy()
            resolvedTowed = towedVehicle
        elseif resolvedTowed == nil then
            resolvedTowed = towedVehicle
        end

        if resolvedTowing == nil and resolvedTowed ~= nil then
            resolvedTowing = resolvedTowed:getVehicleTowedBy()
        end

        if resolvedTowing ~= nil and resolvedTowed ~= nil and resolvedTowed:getVehicleTowedBy() ~= resolvedTowing then
            local directRear = resolvedTowing:getVehicleTowing()
            if directRear ~= nil and directRear:getVehicleTowedBy() == resolvedTowing then
                resolvedTowed = directRear
            else
                resolvedTowing = nil
                resolvedTowed = nil
            end
        end

        return resolvedTowing, resolvedTowed
    end

    local originalPerformDetach = getTowbarPerformDetachHook()
    if originalPerformDetach and originalPerformDetach ~= TowBarMod.Hook._landtrainPerformDetachWrapper then
        local performDetachWrapper = function(playerObj, towingVehicle, towedVehicle)
            local resolvedTowingVehicle, resolvedTowedVehicle = resolveTowbarDetachPair(towingVehicle, towedVehicle)
            if resolvedTowingVehicle == nil or resolvedTowedVehicle == nil then
                resolvedTowingVehicle = towingVehicle
                resolvedTowedVehicle = towedVehicle
            end
            if resolvedTowingVehicle == nil or resolvedTowedVehicle == nil then
                return
            end

            dumpTowState("performDetachTowBar pre towing", resolvedTowingVehicle)
            dumpTowState("performDetachTowBar pre towed", resolvedTowedVehicle)
            originalPerformDetach(playerObj, resolvedTowingVehicle, resolvedTowedVehicle)
            dumpTowState("performDetachTowBar post-original towing", resolvedTowingVehicle)
            dumpTowState("performDetachTowBar post-original towed", resolvedTowedVehicle)
            clearLandtrainFrontLinkData(resolvedTowedVehicle)

            reconcileTowbarSplitAround(resolvedTowingVehicle)
            reconcileTowbarSplitAround(resolvedTowedVehicle)
            queueTowbarSplitReconcile(playerObj, resolvedTowingVehicle, 12)
            queueTowbarSplitReconcile(playerObj, resolvedTowedVehicle, 12)
            queueTowbarSplitReconcile(playerObj, resolvedTowingVehicle, 30)
            queueTowbarSplitReconcile(playerObj, resolvedTowedVehicle, 30)

            setTowBarModelVisibleForVehicle(resolvedTowedVehicle, false)
            refreshTowBarState(resolvedTowingVehicle)
            refreshTowBarState(resolvedTowedVehicle)
            refreshTowBarState(resolvedTowingVehicle:getVehicleTowedBy())
            refreshTowBarState(resolvedTowingVehicle:getVehicleTowing())
            refreshTowBarState(resolvedTowedVehicle:getVehicleTowedBy())
            refreshTowBarState(resolvedTowedVehicle:getVehicleTowing())
            saveActiveLandtrainTowbarSnapshot()

            dumpTowState("performDetachTowBar post-reconcile towing", resolvedTowingVehicle)
            dumpTowState("performDetachTowBar post-reconcile towed", resolvedTowedVehicle)
        end

        TowBarMod.Hook.performDetachTowBar = performDetachWrapper
        TowBarMod.Hook.performDeattachTowBar = performDetachWrapper
        TowBarMod.Hook._landtrainPerformDetachWrapper = performDetachWrapper
        ltLog("installLandtrainTowbarPatch patched TowBarMod.Hook.performDetachTowBar")
    end

    -- If vanilla detach sneaks into the radial menu, redirect it to Towbar timed detach.
    if ISVehicleMenu and ISVehicleMenu.onDetachTrailer and ISVehicleMenu.onDetachTrailer ~= TowBarMod.UI._landtrainDetachRedirectWrapper then
        local originalOnDetachTrailer = ISVehicleMenu.onDetachTrailer
        local detachRedirectWrapper = function(playerObj, vehicle, ...)
            local vehicleToCheck = vehicle
            if vehicleToCheck == nil and playerObj and ISVehicleMenu.getVehicleToInteractWith then
                vehicleToCheck = ISVehicleMenu.getVehicleToInteractWith(playerObj)
            end

            local towbarDetachTarget = getTowbarDetachTargetVehicle(vehicleToCheck)
            local detachAction = getTowbarDetachAction()
            if playerObj ~= nil and towbarDetachTarget ~= nil and detachAction ~= nil then
                detachAction(playerObj, towbarDetachTarget)
                return
            end

            return originalOnDetachTrailer(playerObj, vehicle, ...)
        end

        ISVehicleMenu.onDetachTrailer = detachRedirectWrapper
        TowBarMod.UI._landtrainDetachRedirectWrapper = detachRedirectWrapper
        ltLog("installLandtrainTowbarPatch patched ISVehicleMenu.onDetachTrailer")
    end

    -- Towbar UI only adds attach when fully unlinked; add attach for linked/chain-related interactions too.
    if ISVehicleMenu and ISVehicleMenu.showRadialMenu and ISVehicleMenu.showRadialMenu ~= TowBarMod.UI._landtrainShowRadialWrapper then
        local originalShowRadialMenu = ISVehicleMenu.showRadialMenu
        local showRadialWrapper = function(playerObj)
            originalShowRadialMenu(playerObj)

            if playerObj == nil or playerObj:getVehicle() then return end
            local vehicle = ISVehicleMenu.getVehicleToInteractWith(playerObj)
            if vehicle == nil then return end

            local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
            ltLog("showRadial vehicle=" .. vehLabel(vehicle)
                .. " towing=" .. vehLabel(vehicle:getVehicleTowing())
                .. " towedBy=" .. vehLabel(vehicle:getVehicleTowedBy()))
            if (vehicle:getVehicleTowing() or vehicle:getVehicleTowedBy()) and isTowbarChainVehicleOrNeighbor(vehicle) then
                if TowBarMod and TowBarMod.UI and TowBarMod.UI.removeDefaultDetachOption then
                    TowBarMod.UI.removeDefaultDetachOption(playerObj)
                end
                addLandtrainUnhookOptionToMenu(playerObj, vehicle)
            end

            if menuHasTowbarAttachSlice(menu) then
                ltLog("showRadial attach slice already present for " .. vehLabel(vehicle) .. ", skipping Landtrain add")
                return
            end

            local vehicleLinked = vehicle:getVehicleTowing() ~= nil or vehicle:getVehicleTowedBy() ~= nil
            local chainRelated = isTowbarChainVehicleOrNeighbor(vehicle)
            local shouldInjectAttach = vehicleLinked or chainRelated
            ltLog("showRadial attach check vehicle=" .. vehLabel(vehicle)
                .. " linked=" .. tostring(vehicleLinked)
                .. " chainRelated=" .. tostring(chainRelated)
                .. " shouldInject=" .. tostring(shouldInjectAttach))

            if shouldInjectAttach then
                addLandtrainHookOptionToMenu(playerObj, vehicle)
            else
                ltLog("showRadial vehicle not linked, Landtrain attach not injected for " .. vehLabel(vehicle))
            end
        end

        ISVehicleMenu.showRadialMenu = showRadialWrapper
        TowBarMod.UI._landtrainShowRadialWrapper = showRadialWrapper
        ltLog("installLandtrainTowbarPatch patched ISVehicleMenu.showRadialMenu")
    end

    TowBarMod.Utils._landtrainUnlimitedChainsInstalled = true
    return true
end

local landtrainInstallWatchTicks = 0
local landtrainInstallReadyLogged = false

local function isLandtrainRadialPatchActive()
    return TowBarMod ~= nil
        and TowBarMod.UI ~= nil
        and TowBarMod.UI._landtrainShowRadialWrapper ~= nil
        and ISVehicleMenu ~= nil
        and ISVehicleMenu.showRadialMenu == TowBarMod.UI._landtrainShowRadialWrapper
end

local function landtrainInstallWatchdogTick()
    if landtrainInstallWatchTicks <= 0 then
        Events.OnTick.Remove(landtrainInstallWatchdogTick)
        if not isLandtrainRadialPatchActive() then
            ltInfo("Landtrain install watchdog expired before radial hook was active")
        end
        return
    end
    landtrainInstallWatchTicks = landtrainInstallWatchTicks - 1

    local installed = installLandtrainTowbarPatch()
    if installed and isLandtrainRadialPatchActive() then
        if not landtrainInstallReadyLogged then
            ltInfo("Landtrain Towbar hooks active")
            landtrainInstallReadyLogged = true
        end
        Events.OnTick.Remove(landtrainInstallWatchdogTick)
        return
    end

    if (landtrainInstallWatchTicks % 120) == 0 then
        ltInfo("Landtrain waiting for Towbar hooks. ticksLeft=" .. tostring(landtrainInstallWatchTicks))
    end
end

local function startLandtrainInstallWatchdog()
    landtrainInstallReadyLogged = false
    landtrainInstallWatchTicks = 1800 -- 30 seconds at 60fps.
    Events.OnTick.Remove(landtrainInstallWatchdogTick)
    Events.OnTick.Add(landtrainInstallWatchdogTick)

    local installed = installLandtrainTowbarPatch()
    if installed and isLandtrainRadialPatchActive() then
        ltInfo("Landtrain Towbar hooks active")
        landtrainInstallReadyLogged = true
        Events.OnTick.Remove(landtrainInstallWatchdogTick)
    else
        ltInfo("Landtrain install watchdog started")
    end
end

Events.OnGameBoot.Add(ensureTowAttachmentsForTrailers)
Events.OnGameBoot.Add(captureTowbarAttachmentDefaults)
Events.OnGameBoot.Add(startLandtrainInstallWatchdog)
Events.OnGameStart.Add(captureTowbarAttachmentDefaults)
Events.OnGameStart.Add(startLandtrainInstallWatchdog)
Events.OnGameStart.Add(scheduleLandtrainSingleLoadRestore)
Events.OnTick.Add(onLandtrainSaveSnapshotTick)
