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 local function resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData) if not towingVehicle or not towedVehicle then return nil, nil end local attachmentA = towingVehicle:getTowAttachmentSelf() or "trailer" local attachmentB = towingVehicle:getTowAttachmentOther() or (towedModData and towedModData["towBarChangedAttachmentId"]) 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 return attachmentA, attachmentB end local function hasTowBarTowState(modData) if not modData then return false end if modData["isTowingByTowBar"] and modData["towed"] then return true end -- Rejoin fallback: legacy saves may only have the original-script marker. if modData.towBarOriginalScriptName ~= nil then return true end return false end local function isActiveTowBarTowedVehicle(vehicle, modData) if not vehicle or not modData then return false end if modData["isTowingByTowBar"] and modData["towed"] then return true end -- Rejoin fallback: if the tow link exists, original-script marker is enough. if vehicle:getVehicleTowedBy() and modData.towBarOriginalScriptName ~= nil then return true end return false end local function reattachTowBarPair(playerObj, towingVehicle, towedVehicle, requireDriver) if not playerObj or not towingVehicle or not towedVehicle then return false end if requireDriver and not towingVehicle:isDriver(playerObj) then return false end local towingModData = towingVehicle:getModData() local towedModData = towedVehicle:getModData() if not towingModData or not towedModData then return false end if requireDriver then if not isTowBarTowPair(towingVehicle, towedVehicle) then return false end else if not isActiveTowBarTowedVehicle(towedVehicle, towedModData) then return false end end local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData) if not attachmentA or not attachmentB then return false end local towingScript = towingVehicle:getScript() local towedScript = towedVehicle:getScript() if not towingScript or not towedScript then return false end if not towingScript:getAttachmentById(attachmentA) or not towedScript:getAttachmentById(attachmentB) then return false 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)) return true end local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft) if not vehicle then return end local modData = vehicle:getModData() if not hasTowBarTowState(modData) then return end local retries = tonumber(retriesLeft) or 0 local localPlayer = playerObj or getPlayer() local towingVehicle = vehicle:getVehicleTowedBy() if localPlayer and towingVehicle then if reattachTowBarPair(localPlayer, towingVehicle, vehicle, false) then return end end if localPlayer and retries > 0 then -- During world load, tow links can become available a few ticks later. ISTimedActionQueue.add(TowBarScheduleAction:new(localPlayer, 10, recoverTowBarVehicleAfterLoad, vehicle, retries - 1)) return end -- Fallback: keep original post-attach restoration behavior. setTowBarModelVisible(vehicle, true) TowBarMod.Hook.setVehiclePostAttach(nil, vehicle) end function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, retriesLeft) if not towedVehicle then return end local towedModData = towedVehicle:getModData() if not isActiveTowBarTowedVehicle(towedVehicle, towedModData) then return end if towedModData and towedModData.towBarOriginalScriptName then towedVehicle:setScriptName(towedModData.towBarOriginalScriptName) end local towingVehicle = towedVehicle:getVehicleTowedBy() if towingVehicle then local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData) if attachmentA and attachmentB then TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB) end 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 local towedVehicle = towingVehicle:getVehicleTowing() if not towedVehicle then return end reattachTowBarPair(playerObj, towingVehicle, towedVehicle, true) 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) recoverTowBarVehicleAfterLoad(nil, vehicle, 6) end function TowBarMod.Hook.OnGameStart() local cell = getCell() if not cell then return end local vehicles = cell:getVehicles() if not vehicles then return end local playerObj = getPlayer() for i = 0, vehicles:size() - 1 do recoverTowBarVehicleAfterLoad(playerObj, vehicles:get(i), 6) end end Events.OnSpawnVehicleEnd.Add(TowBarMod.Hook.OnSpawnVehicle) if Events.OnGameStart then Events.OnGameStart.Add(TowBarMod.Hook.OnGameStart) end Events.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle) Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat)