if not TowBarMod then TowBarMod = {} end if not TowBarMod.Hook then TowBarMod.Hook = {} end local DefaultTowBarTowMass = 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 FreeRollTickInterval = 15 local freeRollTickCounter = 0 local function tryVehicleCall(vehicle, methodName, arg) if not vehicle or not methodName then return false, nil end local method = vehicle[methodName] if method == nil then return false, nil end return pcall(function() if arg ~= nil then return method(vehicle, arg) end return method(vehicle) end) end local function storeOriginalVehicleCall(vehicle, modData, key, getterName) if not vehicle or not modData or modData[key] ~= nil then return end local ok, value = tryVehicleCall(vehicle, getterName) if ok and value ~= nil then modData[key] = value end end local function applyFreeRollingTowState(vehicle) if not vehicle then return end local modData = vehicle:getModData() if not modData then return end if modData.towBarOriginalMass == nil then modData.towBarOriginalMass = vehicle:getMass() end if modData.towBarOriginalBrakingForce == nil then modData.towBarOriginalBrakingForce = vehicle:getBrakingForce() end storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrakeOn", "isParkingBrakeOn") storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrake", "isParkingBrake") storeOriginalVehicleCall(vehicle, modData, "towBarOriginalHandbrake", "isHandbrake") local configuredTowMass = TowBarMod.Config and tonumber(TowBarMod.Config.towedVehicleRollingMass) vehicle:setMass(configuredTowMass or DefaultTowBarTowMass) vehicle:setBrakingForce(0) if modData.towBarOriginalParkingBrakeOn ~= nil then tryVehicleCall(vehicle, "setParkingBrakeOn", false) end if modData.towBarOriginalParkingBrake ~= nil then tryVehicleCall(vehicle, "setParkingBrake", false) end if modData.towBarOriginalHandbrake ~= nil then tryVehicleCall(vehicle, "setHandbrake", false) end vehicle:constraintChanged() vehicle:updateTotalMass() end local function restoreFreeRollingTowState(vehicle, modData) if not vehicle or not modData then return end if modData.towBarOriginalMass ~= nil then vehicle:setMass(modData.towBarOriginalMass) end if modData.towBarOriginalBrakingForce ~= nil then vehicle:setBrakingForce(modData.towBarOriginalBrakingForce) end if modData.towBarOriginalParkingBrakeOn ~= nil then tryVehicleCall(vehicle, "setParkingBrakeOn", modData.towBarOriginalParkingBrakeOn) end if modData.towBarOriginalParkingBrake ~= nil then tryVehicleCall(vehicle, "setParkingBrake", modData.towBarOriginalParkingBrake) end if modData.towBarOriginalHandbrake ~= nil then tryVehicleCall(vehicle, "setHandbrake", modData.towBarOriginalHandbrake) end vehicle:constraintChanged() vehicle:updateTotalMass() end 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 sendTowAttachCommand(playerObj, args) if not playerObj or not args then return end -- MP-safe/server-authoritative attach path (Landtrain style). if isClient() and isMultiplayer() then sendClientCommand(playerObj, "towbar", "attachTowBar", args) return end -- Keep vanilla attach path for SP/local behavior. sendClientCommand(playerObj, "vehicle", "attachTrailer", args) end local TowbarVariantSize = 24 local TowbarMaxIndex = TowbarVariantSize - 1 local TowbarFirstZ = 1.0 local TowbarSlotStep = 0.1 -- Measured from Towbar.fbx at its original 0.01 script scale. The visual is -- enlarged uniformly, so its center must move outward by the scaled half-length -- to keep the inner end on the same vehicle-hitbox contact point. local TowbarVisualScale = 2.5 local TowbarModelLength = 0.9714089036 local TowbarScaledModelLength = TowbarModelLength * TowbarVisualScale local TowbarModelHalfLength = TowbarScaledModelLength / 2 local function getTowbarFrontEdgeZ(script) if not script then return nil end local ok, shape = pcall(function() return script:getPhysicsChassisShape() end) if not ok or not shape then return nil end local shapeOk, shapeZ = pcall(function() return shape:z() end) if not shapeOk or type(shapeZ) ~= "number" or shapeZ <= 0 then return nil end local centerOk, center = pcall(function() return script:getCenterOfMassOffset() end) if not centerOk or not center then return nil end local zOk, centerZ = pcall(function() return center:z() end) if not zOk or type(centerZ) ~= "number" then return nil end return centerZ + shapeZ / 2 end local function getTowbarModelSlot(script) local frontEdgeZ = getTowbarFrontEdgeZ(script) if frontEdgeZ == nil then return nil end -- Model offsets position the mesh origin. Move its center outward so the -- towbar's inner end, rather than its center, meets the vehicle hitbox. local modelCenterZ = frontEdgeZ + TowbarModelHalfLength local index = math.floor(((modelCenterZ - TowbarFirstZ) / TowbarSlotStep) + 0.5) return math.max(0, math.min(TowbarMaxIndex, index)) end local function setTowBarModelVisible(vehicle, isVisible) if not vehicle then return end local normalPart = vehicle:getPartById("towbar") local largePart = vehicle:getPartById("towbarLarge") if normalPart == nil and largePart == nil then return end for j = 0, TowbarVariantSize - 1 do if normalPart then normalPart:setModelVisible("towbar" .. j, false) end if largePart then largePart:setModelVisible("towbar" .. j, false) end end if not isVisible then vehicle:doDamageOverlay() return end local script = vehicle:getScript() if not script then vehicle:doDamageOverlay() return end local index = getTowbarModelSlot(script) local part = normalPart if part and index ~= nil then part:setModelVisible("towbar" .. index, true) end vehicle:doDamageOverlay() end function TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicle, scriptName) if not vehicle or not scriptName then return false end local modData = vehicle:getModData() setTowBarModelVisible(vehicle, false) if modData then modData.towBarModelSwapInProgress = true end local ok, err = pcall(function() vehicle:setScriptName(scriptName) end) if modData then modData.towBarModelSwapInProgress = nil end -- Script initialization can reset part visibility, so hide it again before -- allowing the intended model to be shown by the caller. setTowBarModelVisible(vehicle, false) if not ok then error(err) end return true 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() applyFreeRollingTowState(towedVehicle) towingModData["isTowingByTowBar"] = true towingModData["towed"] = false towingModData["towBarTowedVehicleId"] = towedVehicle:getId() towingModData["towBarTowingVehicleId"] = nil towingModData["towBarExpectedAttachment"] = attachmentA towedModData["isTowingByTowBar"] = true towedModData["towed"] = true towedModData["towBarTowedVehicleId"] = nil towedModData["towBarTowingVehicleId"] = towingVehicle:getId() towedModData["towBarExpectedAttachment"] = attachmentB towingVehicle:transmitModData() towedVehicle:transmitModData() TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, "notTowingA_Trailer") local args = { vehicleA = towingVehicle:getId(), vehicleB = towedVehicle:getId(), attachmentA = attachmentA, attachmentB = attachmentB } sendTowAttachCommand(playerObj, args) ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle)) return true end local function reattachTowBarPairAfterCleanDetach(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 detachArgs = { towingVehicle = towingVehicle:getId(), vehicle = towedVehicle:getId() } sendClientCommand(playerObj, "towbar", "detachTowBar", detachArgs) -- World load/spawn can restore constraints in a bad state. Reattach one -- short tick later so the detach is fully applied first. ISTimedActionQueue.add(TowBarScheduleAction:new( playerObj, 1, reattachTowBarPair, towingVehicle, towedVehicle, requireDriver )) 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 towingVehicle then -- Apply rigid spacing as soon as the tow link exists to avoid a visible -- bumper-to-bumper snap. Server/SP reconciliation owns recovery and -- never refunds or consumes an item while loading a save. TowBarMod.Hook.setVehiclePostAttach(nil, vehicle) return 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 -- Leave reciprocal saved state untouched. The authoritative audit will -- reconnect it when both vehicle chunks are loaded. 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 TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, towedModData.towBarOriginalScriptName) end local towingVehicle = towedVehicle:getVehicleTowedBy() if towingVehicle then local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData) if attachmentA and attachmentB then local towingModData = towingVehicle:getModData() if towingModData then towingModData["towBarTowedVehicleId"] = towedVehicle:getId() towingModData["towBarTowingVehicleId"] = nil towingModData["towBarExpectedAttachment"] = attachmentA towedModData["towBarTowingVehicleId"] = towingVehicle:getId() towedModData["towBarTowedVehicleId"] = nil towedModData["towBarExpectedAttachment"] = attachmentB towingVehicle:transmitModData() towedVehicle:transmitModData() end TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB) end end applyFreeRollingTowState(towedVehicle) -- Re-show the towbar model after the script name has been restored. -- setScriptName() resets model visibility, so we must set it again here. setTowBarModelVisible(towedVehicle, true) 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 and not (isClient() and isMultiplayer()) 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() applyFreeRollingTowState(towedVehicle) towingModData["isTowingByTowBar"] = true towingModData["towed"] = false towingModData["towBarTowedVehicleId"] = towedVehicle:getId() towingModData["towBarTowingVehicleId"] = nil towingModData["towBarExpectedAttachment"] = attachmentA towedModData["isTowingByTowBar"] = true towedModData["towed"] = true towedModData["towBarTowedVehicleId"] = nil towedModData["towBarTowingVehicleId"] = towingVehicle:getId() towedModData["towBarExpectedAttachment"] = attachmentB towingVehicle:transmitModData() towedVehicle:transmitModData() -- Match the known-good rigid tow path: fake trailer + vanilla attach command. TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, "notTowingA_Trailer") local args = { vehicleA = towingVehicle:getId(), vehicleB = towedVehicle:getId(), attachmentA = attachmentA, attachmentB = attachmentB, itemId = towBarItem and towBarItem:getID() or nil } sendTowAttachCommand(playerObj, args) ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle)) end function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle) if towingVehicle == nil or towedVehicle == nil then return end local towingModData = towingVehicle:getModData() local towedModData = towedVehicle:getModData() if not towingModData or not towedModData then return end if towedModData.towBarOriginalScriptName then TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, towedModData.towBarOriginalScriptName) end -- A very fast break can happen while the temporary fake-trailer script is -- still active. Restore the real script first so the saved offsets are -- written back to the actual vehicle attachments, not the temporary script. TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle) restoreFreeRollingTowState(towedVehicle, towedModData) towingModData["isTowingByTowBar"] = false towingModData["towed"] = false towingModData["towBarTowedVehicleId"] = nil towingModData["towBarTowedVehicleSqlId"] = nil towingModData["towBarTowingVehicleId"] = nil towingModData["towBarTowingVehicleSqlId"] = nil towingModData["towBarExpectedAttachment"] = nil towedModData["isTowingByTowBar"] = false towedModData["towed"] = false towedModData["towBarTowedVehicleId"] = nil towedModData["towBarTowedVehicleSqlId"] = nil towedModData["towBarTowingVehicleId"] = nil towedModData["towBarTowingVehicleSqlId"] = nil towedModData["towBarExpectedAttachment"] = nil towedModData.towBarOriginalScriptName = nil towedModData.towBarOriginalMass = nil towedModData.towBarOriginalBrakingForce = nil towedModData.towBarOriginalParkingBrakeOn = nil towedModData.towBarOriginalParkingBrake = nil towedModData.towBarOriginalHandbrake = nil towingVehicle:transmitModData() towedVehicle:transmitModData() TowBarMod.Hook.lastAutoReattachAtByVehicle[towingVehicle:getId()] = nil setTowBarModelVisible(towedVehicle, false) end function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehicle) if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end local args = { towingVehicle = towingVehicle:getId(), vehicle = towedVehicle:getId() } sendClientCommand(playerObj, "towbar", "detachTowBar", args) TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle) 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 local function forEachCollectionItem(collection, callback) if not collection then return end local ok, iterator = pcall(function() return collection:iterator() end) if ok and iterator then while iterator:hasNext() do callback(iterator:next()) end return end local size ok, size = pcall(function() return collection:size() end) if not ok or type(size) ~= "number" then return end for i = 0, size - 1 do callback(collection:get(i)) end end local function keepTowBarVehiclesFreeRolling() freeRollTickCounter = freeRollTickCounter + 1 if freeRollTickCounter < FreeRollTickInterval then return end freeRollTickCounter = 0 local cell = getCell() if not cell then return end local vehicles = cell:getVehicles() if not vehicles then return end forEachCollectionItem(vehicles, function(vehicle) local modData = vehicle and vehicle:getModData() or nil if isActiveTowBarTowedVehicle(vehicle, modData) then applyFreeRollingTowState(vehicle) end end) 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() forEachCollectionItem(vehicles, function(vehicle) recoverTowBarVehicleAfterLoad(playerObj, vehicle, 6) end) end --------------------------------------------------------------------------- --- Dev / debug helpers --------------------------------------------------------------------------- function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle) if not vehicle then return end local normalPart = vehicle:getPartById("towbar") local largePart = vehicle:getPartById("towbarLarge") if normalPart == nil and largePart == nil then print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName())) return end local script = vehicle:getScript() local chassisZ = script and script:getPhysicsChassisShape():z() or 0 local halfZ = chassisZ / 2 local index = 0 if script then index = getTowbarModelSlot(script) end local selectedPart = "towbar" print("[TowBar DEV] Vehicle: " .. tostring(vehicle:getScriptName())) print("[TowBar DEV] chassisShape.z = " .. tostring(chassisZ) .. ", half = " .. tostring(halfZ)) print("[TowBar DEV] frontEdgeZ = " .. tostring(script and getTowbarFrontEdgeZ(script) or nil) .. ", part = " .. selectedPart) print("[TowBar DEV] Formula picks index = " .. tostring(index) .. " (towbar" .. tostring(index) .. " at Z offset " .. tostring(1.0 + index * 0.1) .. ")") print("[TowBar DEV] Showing towbar0..towbar23 on both parts") for j = 0, TowbarVariantSize - 1 do if normalPart then normalPart:setModelVisible("towbar" .. j, true) end if largePart then largePart:setModelVisible("towbar" .. j, true) end end vehicle:doDamageOverlay() end function TowBarMod.Hook.devHideAllTowbarModels(playerObj, vehicle) if not vehicle then return end local normalPart = vehicle:getPartById("towbar") local largePart = vehicle:getPartById("towbarLarge") if normalPart == nil and largePart == nil then print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName())) return end print("[TowBar DEV] Hiding ALL towbar models on " .. tostring(vehicle:getScriptName())) for j = 0, TowbarVariantSize - 1 do if normalPart then normalPart:setModelVisible("towbar" .. j, false) end if largePart then largePart:setModelVisible("towbar" .. j, false) end end vehicle:doDamageOverlay() end function TowBarMod.Hook.devShowSingleTowbar(playerObj, vehicle, index) if not vehicle then return end local normalPart = vehicle:getPartById("towbar") local largePart = vehicle:getPartById("towbarLarge") if normalPart == nil and largePart == nil then print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName())) return end local localIndex = math.max(0, math.min(TowbarMaxIndex, index % TowbarVariantSize)) local useLargePart = index >= TowbarVariantSize for j = 0, TowbarVariantSize - 1 do if normalPart then normalPart:setModelVisible("towbar" .. j, false) end if largePart then largePart:setModelVisible("towbar" .. j, false) end end local part = useLargePart and largePart or normalPart if part == nil then part = normalPart or largePart end print("[TowBar DEV] Showing only towbar" .. tostring(localIndex) .. " on part " .. tostring(useLargePart and "towbarLarge" or "towbar") .. " (Z offset " .. tostring(1.0 + localIndex * 0.1) .. ") on " .. tostring(vehicle:getScriptName())) if part then part:setModelVisible("towbar" .. localIndex, true) end vehicle:doDamageOverlay() 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) Events.OnTick.Add(keepTowBarVehiclesFreeRolling)