if not TowBarMod then TowBarMod = {} end if not TowBarMod.Hook then TowBarMod.Hook = {} end local TowBarTowMass = 200 local AutoReattachCooldownHours = 1 / 7200 -- 0.5 seconds TowBarMod.Hook.lastAutoReattachAtByVehicle = TowBarMod.Hook.lastAutoReattachAtByVehicle or {} local AutoReattachPlayerCooldownHours = 1 / 14400 -- 0.25 seconds TowBarMod.Hook.lastAutoReattachAtByPlayer = TowBarMod.Hook.lastAutoReattachAtByPlayer or {} local function isTowBarTowPair(towingVehicle, towedVehicle) if not towingVehicle or not towedVehicle then return false end local towingModData = towingVehicle:getModData() local towedModData = towedVehicle:getModData() if not towingModData or not towedModData then return false end if towingModData["isTowingByTowBar"] and towedModData["isTowingByTowBar"] and towedModData["towed"] then return true end -- Rejoin fallback: original towbar state on the towed vehicle is enough to reapply rigid spacing. if towedModData.towBarOriginalScriptName ~= nil then return true end return false end local function getTowBarItem(playerObj) if not playerObj then return nil end local inventory = playerObj:getInventory() if not inventory then return nil end return inventory:getItemFromTypeRecurse("TowBar.TowBar") end local function setTowBarModelVisible(vehicle, isVisible) if not vehicle 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 not isVisible then vehicle:doDamageOverlay() return end local script = vehicle:getScript() if not script then vehicle:doDamageOverlay() return end local scale = script:getModelScale() if scale >= 1.5 and scale <= 2 then local z = script:getPhysicsChassisShape():z()/2 - 0.1 part:setModelVisible("towbar" .. math.floor((z*2/3 - 1)*10), true) end vehicle:doDamageOverlay() end function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle) if not towedVehicle then return end local towedModData = towedVehicle:getModData() if towedModData and towedModData.towBarOriginalScriptName then towedVehicle:setScriptName(towedModData.towBarOriginalScriptName) end towedVehicle:setMass(TowBarTowMass) towedVehicle:setBrakingForce(0) towedVehicle:constraintChanged() towedVehicle:updateTotalMass() end function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end local towBarItem = getTowBarItem(playerObj) if towBarItem ~= nil then sendClientCommand(playerObj, "towbar", "consumeTowBar", { itemId = towBarItem:getID() }) end playerObj:setPrimaryHandItem(nil) TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB) local towingModData = towingVehicle:getModData() local towedModData = towedVehicle:getModData() towedModData.towBarOriginalScriptName = towedVehicle:getScriptName() towedModData.towBarOriginalMass = towedVehicle:getMass() towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce() towingModData["isTowingByTowBar"] = true towedModData["isTowingByTowBar"] = true towedModData["towed"] = true towingVehicle:transmitModData() towedVehicle:transmitModData() setTowBarModelVisible(towedVehicle, true) -- Match the known-good rigid tow path: fake trailer + vanilla attach command. towedVehicle:setScriptName("notTowingA_Trailer") local args = { vehicleA = towingVehicle:getId(), vehicleB = towedVehicle:getId(), attachmentA = attachmentA, attachmentB = attachmentB } sendClientCommand(playerObj, "vehicle", "attachTrailer", args) ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle)) end function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehicle) if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle) local args = { towingVehicle = towingVehicle:getId(), vehicle = towedVehicle:getId() } sendClientCommand(playerObj, "towbar", "detachTowBar", args) local towedModData = towedVehicle:getModData() if towedModData.towBarOriginalScriptName then towedVehicle:setScriptName(towedModData.towBarOriginalScriptName) end if towedModData.towBarOriginalMass ~= nil then towedVehicle:setMass(towedModData.towBarOriginalMass) end if towedModData.towBarOriginalBrakingForce ~= nil then towedVehicle:setBrakingForce(towedModData.towBarOriginalBrakingForce) end towedVehicle:constraintChanged() towedVehicle:updateTotalMass() sendClientCommand(playerObj, "towbar", "giveTowBar", { equipPrimary = true }) local towingModData = towingVehicle:getModData() towingModData["isTowingByTowBar"] = false towedModData["isTowingByTowBar"] = false towedModData["towed"] = false towedModData.towBarOriginalScriptName = nil towedModData.towBarOriginalMass = nil towedModData.towBarOriginalBrakingForce = nil towingVehicle:transmitModData() towedVehicle:transmitModData() TowBarMod.Hook.lastAutoReattachAtByVehicle[towingVehicle:getId()] = nil setTowBarModelVisible(towedVehicle, false) end function TowBarMod.Hook.reattachTowBarFromDriverSeat(playerObj, towingVehicle) if not playerObj or not towingVehicle then return end if not towingVehicle:isDriver(playerObj) then return end local towedVehicle = towingVehicle:getVehicleTowing() if not towedVehicle then return end local towingModData = towingVehicle:getModData() local towedModData = towedVehicle:getModData() if not towingModData or not towedModData then return end if not isTowBarTowPair(towingVehicle, towedVehicle) then return end local attachmentA = towingVehicle:getTowAttachmentSelf() or "trailer" local attachmentB = towingVehicle:getTowAttachmentOther() or "trailerfront" if not towingVehicle:canAttachTrailer(towedVehicle, attachmentA, attachmentB) then if towingVehicle:canAttachTrailer(towedVehicle, "trailer", "trailerfront") then attachmentA = "trailer" attachmentB = "trailerfront" elseif towingVehicle:canAttachTrailer(towedVehicle, "trailerfront", "trailer") then attachmentA = "trailerfront" attachmentB = "trailer" end end TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB) towedModData.towBarOriginalScriptName = towedModData.towBarOriginalScriptName or towedVehicle:getScriptName() if towedModData.towBarOriginalMass == nil then towedModData.towBarOriginalMass = towedVehicle:getMass() end if towedModData.towBarOriginalBrakingForce == nil then towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce() end towingModData["isTowingByTowBar"] = true towedModData["isTowingByTowBar"] = true towedModData["towed"] = true towingVehicle:transmitModData() towedVehicle:transmitModData() setTowBarModelVisible(towedVehicle, true) towedVehicle:setScriptName("notTowingA_Trailer") local args = { vehicleA = towingVehicle:getId(), vehicleB = towedVehicle:getId(), attachmentA = attachmentA, attachmentB = attachmentB } sendClientCommand(playerObj, "vehicle", "attachTrailer", args) ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle)) end local function tryAutoReattachFromCharacter(character) if not character or not instanceof(character, "IsoPlayer") or not character:isLocalPlayer() then return end local playerObj = character local nowHours = getGameTime() and getGameTime():getWorldAgeHours() or 0 local playerNum = playerObj:getPlayerNum() local lastPlayerHours = TowBarMod.Hook.lastAutoReattachAtByPlayer[playerNum] if lastPlayerHours and (nowHours - lastPlayerHours) < AutoReattachPlayerCooldownHours then return end local towingVehicle = playerObj:getVehicle() if not towingVehicle then return end if not towingVehicle:isDriver(playerObj) then return end local towedVehicle = towingVehicle:getVehicleTowing() if not towedVehicle then return end if not isTowBarTowPair(towingVehicle, towedVehicle) then return end local vehicleId = towingVehicle:getId() local lastHours = TowBarMod.Hook.lastAutoReattachAtByVehicle[vehicleId] if lastHours and (nowHours - lastHours) < AutoReattachCooldownHours then return end TowBarMod.Hook.lastAutoReattachAtByPlayer[playerNum] = nowHours TowBarMod.Hook.lastAutoReattachAtByVehicle[vehicleId] = nowHours TowBarMod.Hook.reattachTowBarFromDriverSeat(playerObj, towingVehicle) end function TowBarMod.Hook.OnEnterVehicle(character) tryAutoReattachFromCharacter(character) end function TowBarMod.Hook.OnSwitchVehicleSeat(character) tryAutoReattachFromCharacter(character) end function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehicle) if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end local item = getTowBarItem(playerObj) if item == nil then return end if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end local hookPoint = towedVehicle:getAttachmentWorldPos("trailerfront", TowBarMod.Utils.tempVector1) if hookPoint == nil then return end ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z())) if not playerObj:getInventory():contains("TowBar.TowBar") then ISTimedActionQueue.add(ISInventoryTransferAction:new(playerObj, item, item:getContainer(), playerObj:getInventory(), nil)) end local storePrim = playerObj:getPrimaryHandItem() if storePrim == nil or storePrim ~= item then ISTimedActionQueue.add(ISEquipWeaponAction:new(playerObj, item, 12, true)) end ISTimedActionQueue.add(TowBarHookVehicle:new(playerObj, 300, TowBarMod.Config.lowLevelAnimation)) hookPoint = towingVehicle:getAttachmentWorldPos("trailer", TowBarMod.Utils.tempVector1) if hookPoint == nil then return end ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z())) ISTimedActionQueue.add(TowBarHookVehicle:new( playerObj, 100, TowBarMod.Config.lowLevelAnimation, TowBarMod.Hook.performAttachTowBar, towingVehicle, towedVehicle, "trailer", "trailerfront" )) end function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle) local towingVehicle = vehicle local towedVehicle = vehicle and vehicle:getVehicleTowing() or nil if vehicle and vehicle:getVehicleTowedBy() then towingVehicle = vehicle:getVehicleTowedBy() towedVehicle = vehicle end if towingVehicle == nil or towedVehicle == nil then return end local localPoint = towingVehicle:getAttachmentLocalPos(towingVehicle:getTowAttachmentSelf(), TowBarMod.Utils.tempVector1) local shift = 0 if towingVehicle:getModData()["isChangedTowedAttachment"] then shift = localPoint:z() > 0 and -1 or 1 end local hookPoint = towingVehicle:getWorldPos(localPoint:x(), localPoint:y(), localPoint:z() + shift, TowBarMod.Utils.tempVector2) if hookPoint == nil then return end ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z())) local storePrim = playerObj:getPrimaryHandItem() if storePrim ~= nil then ISTimedActionQueue.add(ISUnequipAction:new(playerObj, storePrim, 12)) end ISTimedActionQueue.add(TowBarHookVehicle:new(playerObj, 100, TowBarMod.Config.lowLevelAnimation)) localPoint = towedVehicle:getAttachmentLocalPos(towedVehicle:getTowAttachmentSelf(), TowBarMod.Utils.tempVector1) shift = 0 if towedVehicle:getModData()["isChangedTowedAttachment"] then shift = localPoint:z() > 0 and -1 or 1 end hookPoint = towedVehicle:getWorldPos(localPoint:x(), localPoint:y(), localPoint:z() + shift, TowBarMod.Utils.tempVector2) if hookPoint == nil then return end ISTimedActionQueue.add(TowBarCustomPathFind:pathToLocationF(playerObj, hookPoint:x(), hookPoint:y(), hookPoint:z())) ISTimedActionQueue.add(TowBarHookVehicle:new( playerObj, 300, TowBarMod.Config.lowLevelAnimation, TowBarMod.Hook.performDetachTowBar, towingVehicle, towedVehicle )) end function TowBarMod.Hook.OnSpawnVehicle(vehicle) if not vehicle then return end local modData = vehicle:getModData() if not (modData and modData["isTowingByTowBar"] and modData["towed"]) then return end -- Keep behavior consistent after load/rejoin for active towbar tows. vehicle:setScriptName("notTowingA_Trailer") local playerObj = getPlayer() if playerObj then ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, vehicle)) else TowBarMod.Hook.setVehiclePostAttach(nil, vehicle) end setTowBarModelVisible(vehicle, true) end Events.OnSpawnVehicleEnd.Add(TowBarMod.Hook.OnSpawnVehicle) Events.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle) Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat)