if not TowBarMod then TowBarMod = {} end if not TowBarMod.Utils then TowBarMod.Utils = {} end TowBarMod.Utils.tempVector1 = Vector3f.new() TowBarMod.Utils.tempVector2 = Vector3f.new() --------------------------------------------------------------------------- --- Util functions --------------------------------------------------------------------------- --- Compute the attachment Y offset for a vehicle so the towbar sits just --- above the wheels (i.e. a fixed distance off the ground) regardless of --- how the vehicle model is configured. local function computeAttachmentHeight(vehicle) local script = vehicle:getScript() if not script then return -0.5 end local wheelCount = script:getWheelCount() if wheelCount > 0 then return script:getWheel(0):getOffset():y() + 0.1 end return -0.5 end local function getVehicleHalfLength(script) if not script then return nil end local ok, shape = pcall(function() return script:getPhysicsChassisShape() end) if ok and shape then local zOk, z = pcall(function() return shape:z() end) z = zOk and tonumber(z) or nil if z and z > 0 then return z / 2 end end ok, shape = pcall(function() return script:getExtents() end) if ok and shape then local zOk, z = pcall(function() return shape:z() end) z = zOk and tonumber(z) or nil if z and z > 0 then return z / 2 end end return nil end local function getExteriorAttachmentZ(script, attachmentId, originalZ, extraDistance) local halfLength = getVehicleHalfLength(script) if halfLength == nil then return originalZ end local configuredPadding = TowBarMod.Config and tonumber(TowBarMod.Config.towAttachmentExteriorPadding) local padding = configuredPadding or 0.25 local direction = originalZ >= 0 and 1 or -1 if originalZ == 0 and attachmentId == "trailer" then direction = -1 end local exteriorZ = direction * (halfLength + padding + (extraDistance or 0)) if math.abs(originalZ) > math.abs(exteriorZ) then return originalZ end return exteriorZ end function TowBarMod.Utils.isTrailer(vehicle) return string.match(string.lower(vehicle:getScript():getName()), "trailer") end --- Return vehicles from sector that player can tow by tow bar. function TowBarMod.Utils.getAviableVehicles(mainVehicle, hasTowBar) local vehicles = {} if not hasTowBar then return vehicles end local square = mainVehicle:getSquare() if square == nil then return vehicles end -- Match vanilla towing search radius. 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 and #(TowBarMod.Utils.getHookTypeVariants(mainVehicle, obj, hasTowBar)) ~= 0 then table.insert(vehicles, obj) end end end end end return vehicles end --- Return a table with towbar-only hook options for vehicles. function TowBarMod.Utils.getHookTypeVariants(vehicleA, vehicleB, hasTowBar) local hookTypeVariants = {} if not hasTowBar then return hookTypeVariants end if vehicleA:getVehicleTowing() or vehicleA:getVehicleTowedBy() or vehicleB:getVehicleTowing() or vehicleB:getVehicleTowedBy() then return hookTypeVariants end -- Keep tow bars for vehicle-to-vehicle towing only. if TowBarMod.Utils.isTrailer(vehicleA) or TowBarMod.Utils.isTrailer(vehicleB) then return hookTypeVariants end if vehicleA:canAttachTrailer(vehicleB, "trailerfront", "trailer") then local hookType = {} hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getText("UI_Text_Towing_byTowBar") hookType.func = TowBarMod.Hook.attachByTowBarAction hookType.towingVehicle = vehicleB hookType.towedVehicle = vehicleA hookType.textureName = "tow_bar_icon" table.insert(hookTypeVariants, hookType) elseif vehicleA:canAttachTrailer(vehicleB, "trailer", "trailerfront") then local hookType = {} hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getText("UI_Text_Towing_byTowBar") hookType.func = TowBarMod.Hook.attachByTowBarAction hookType.towingVehicle = vehicleA hookType.towedVehicle = vehicleB hookType.textureName = "tow_bar_icon" table.insert(hookTypeVariants, hookType) end return hookTypeVariants end function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB) local towingScript = towingVehicle:getScript() local towedScript = towedVehicle:getScript() if towingScript == nil or towedScript == nil then return end local towingAttachment = towingScript:getAttachmentById(attachmentA) local towedAttachment = towedScript:getAttachmentById(attachmentB) if towingAttachment == nil or towedAttachment == nil then return end towingAttachment:setUpdateConstraint(false) towingAttachment:setZOffset(0) towedAttachment:setUpdateConstraint(false) towedAttachment:setZOffset(0) -- Dynamic height: compute Y from wheel offset so the towbar never clips the floor. local towingHeight = computeAttachmentHeight(towingVehicle) local towedHeight = computeAttachmentHeight(towedVehicle) -- Store and update the towing vehicle's attachment Y. local towingModData = towingVehicle:getModData() local towingOffset = towingAttachment:getOffset() if towingModData["towBarOriginalTowingAttachmentId"] ~= attachmentA or towingModData["towBarOriginalTowingOffsetX"] == nil or towingModData["towBarOriginalTowingOffsetY"] == nil or towingModData["towBarOriginalTowingOffsetZ"] == nil then towingModData["towBarOriginalTowingOffsetX"] = towingOffset:x() towingModData["towBarOriginalTowingOffsetY"] = towingOffset:y() towingModData["towBarOriginalTowingOffsetZ"] = towingOffset:z() towingModData["towBarOriginalTowingAttachmentId"] = attachmentA end local towedModData = towedVehicle:getModData() local spacingDistance = 1.0 if TowBarMod.Config and tonumber(TowBarMod.Config.rigidTowbarDistance) ~= nil then spacingDistance = tonumber(TowBarMod.Config.rigidTowbarDistance) end local towingBaseX = tonumber(towingModData["towBarOriginalTowingOffsetX"]) or towingOffset:x() local towingBaseZ = tonumber(towingModData["towBarOriginalTowingOffsetZ"]) or towingOffset:z() local towingExteriorZ = getExteriorAttachmentZ(towingScript, attachmentA, towingBaseZ, 0) towingAttachment:getOffset():set(towingBaseX, towingHeight, towingExteriorZ) local offset = towedAttachment:getOffset() local storedBaseX = tonumber(towedModData["towBarBaseAttachmentOffsetX"]) local storedBaseY = tonumber(towedModData["towBarBaseAttachmentOffsetY"]) local storedBaseZ = tonumber(towedModData["towBarBaseAttachmentOffsetZ"]) local hasStoredBase = towedModData["towBarBaseAttachmentId"] == attachmentB and storedBaseX ~= nil and storedBaseY ~= nil and storedBaseZ ~= nil local baseX = hasStoredBase and storedBaseX or offset:x() local baseY = hasStoredBase and storedBaseY or offset:y() local baseZ = hasStoredBase and storedBaseZ or offset:z() if not hasStoredBase then towedModData["towBarBaseAttachmentId"] = attachmentB towedModData["towBarBaseAttachmentOffsetX"] = baseX towedModData["towBarBaseAttachmentOffsetY"] = baseY towedModData["towBarBaseAttachmentOffsetZ"] = baseZ end local towedExteriorZ = getExteriorAttachmentZ(towedScript, attachmentB, baseZ, spacingDistance) local zShift = towedExteriorZ - baseZ towedAttachment:getOffset():set(baseX, towedHeight, towedExteriorZ) towedModData["isChangedTowedAttachment"] = true towedModData["towBarChangedAttachmentId"] = attachmentB towedModData["towBarChangedOffsetZShift"] = zShift towedVehicle:transmitModData() towingVehicle:transmitModData() end function TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle) local towingModData = towingVehicle:getModData() local towingAttachmentId = towingModData["towBarOriginalTowingAttachmentId"] or towingVehicle:getTowAttachmentSelf() local towingAttachment = towingVehicle:getScript():getAttachmentById(towingAttachmentId) if towingAttachment ~= nil then towingAttachment:setUpdateConstraint(true) local zOffset = (towingAttachmentId == "trailer") and -1 or 1 towingAttachment:setZOffset(zOffset) -- Restore the original offset that was overridden for rigid tow spacing. local originalX = tonumber(towingModData["towBarOriginalTowingOffsetX"]) local originalY = tonumber(towingModData["towBarOriginalTowingOffsetY"]) local originalZ = tonumber(towingModData["towBarOriginalTowingOffsetZ"]) if originalX ~= nil and originalY ~= nil and originalZ ~= nil then towingAttachment:getOffset():set(originalX, originalY, originalZ) elseif originalY ~= nil then local off = towingAttachment:getOffset() towingAttachment:getOffset():set(off:x(), originalY, off:z()) end end towingModData["towBarOriginalTowingOffsetX"] = nil towingModData["towBarOriginalTowingOffsetY"] = nil towingModData["towBarOriginalTowingOffsetZ"] = nil towingModData["towBarOriginalTowingAttachmentId"] = nil towingVehicle:transmitModData() local towedModData = towedVehicle:getModData() local changedAttachmentId = towedModData["towBarChangedAttachmentId"] or towedVehicle:getTowAttachmentSelf() local towedAttachment = towedVehicle:getScript():getAttachmentById(changedAttachmentId) if towedAttachment ~= nil then towedAttachment:setUpdateConstraint(true) local zOffset = (changedAttachmentId == "trailer") and -1 or 1 towedAttachment:setZOffset(zOffset) if towedModData["isChangedTowedAttachment"] then local storedBaseX = tonumber(towedModData["towBarBaseAttachmentOffsetX"]) local storedBaseY = tonumber(towedModData["towBarBaseAttachmentOffsetY"]) local storedBaseZ = tonumber(towedModData["towBarBaseAttachmentOffsetZ"]) local hasStoredBase = towedModData["towBarBaseAttachmentId"] == changedAttachmentId and storedBaseX ~= nil and storedBaseY ~= nil and storedBaseZ ~= nil if hasStoredBase then towedAttachment:getOffset():set(storedBaseX, storedBaseY, storedBaseZ) else local offset = towedAttachment:getOffset() local storedShift = tonumber(towedModData["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 towedModData["isChangedTowedAttachment"] = false towedModData["towBarChangedAttachmentId"] = nil towedModData["towBarChangedOffsetZShift"] = nil towedModData["towBarBaseAttachmentId"] = nil towedModData["towBarBaseAttachmentOffsetX"] = nil towedModData["towBarBaseAttachmentOffsetY"] = nil towedModData["towBarBaseAttachmentOffsetZ"] = nil towedVehicle:transmitModData() end ----------------------------------------------------------- --- Fix mods that add vehicles without tow attachments local function fixTowAttachmentsForOtherVehicleMods() local scriptManager = getScriptManager() local vehicleScripts = scriptManager:getAllVehicleScripts() for i = 0, vehicleScripts:size()-1 do local script = vehicleScripts:get(i) local wheelCount = script:getWheelCount() local attachHeigtOffset = -0.5 if wheelCount > 0 then attachHeigtOffset = script:getWheel(0):getOffset():y() + 0.1 end if not string.match(string.lower(script:getName()), "trailer") then local trailerAttachment = script:getAttachmentById("trailer") if trailerAttachment == nil then local attach = ModelAttachment.new("trailer") attach:getOffset():set(0, attachHeigtOffset, -script:getPhysicsChassisShape():z()/2 - 0.1) attach:setZOffset(-1) script:addAttachment(attach) end local trailerFrontAttachment = script:getAttachmentById("trailerfront") if trailerFrontAttachment == nil then local attach = ModelAttachment.new("trailerfront") attach:getOffset():set(0, attachHeigtOffset, script:getPhysicsChassisShape():z()/2 + 0.1) attach:setZOffset(1) script:addAttachment(attach) end end end end Events.OnGameBoot.Add(fixTowAttachmentsForOtherVehicleMods)