if not TowBarMod then TowBarMod = {} end if not TowBarMod.Hook then TowBarMod.Hook = {} end --------------------------------------------------------------------------- --- Tow bar functions --------------------------------------------------------------------------- local TowBarTowMass = 200 local CannotDriveWhileTowedFallbackText = "Cannot drive while being towed" local CannotDriveMessageCooldownHours = 1 / 1800 -- 2 seconds local ForcedTowBarReapplyCooldownHours = 1 / 3600 -- 1 second TowBarMod.Hook.reappliedVehicleIds = TowBarMod.Hook.reappliedVehicleIds or {} TowBarMod.Hook.pendingReapplyVehicleIds = TowBarMod.Hook.pendingReapplyVehicleIds or {} TowBarMod.Hook.reapplyTickCounter = TowBarMod.Hook.reapplyTickCounter or 0 TowBarMod.Hook.lastCannotDriveMessageAtByPlayer = TowBarMod.Hook.lastCannotDriveMessageAtByPlayer or {} TowBarMod.Hook.lastForcedReapplyAtByVehicle = TowBarMod.Hook.lastForcedReapplyAtByVehicle or {} TowBarMod.Hook.lastTowBarVehicleIdByPlayer = TowBarMod.Hook.lastTowBarVehicleIdByPlayer or {} function TowBarMod.Hook.isTowedByTowBar(vehicle) if not vehicle then return false end local modData = vehicle:getModData() if not modData or not modData["isTowingByTowBar"] or not modData["towed"] then return false end local towingVehicle = vehicle:getVehicleTowedBy() if not towingVehicle then return false end local towingModData = towingVehicle:getModData() return towingModData and towingModData["isTowingByTowBar"] == true end function TowBarMod.Hook.getCannotDriveWhileTowedText() local msg = getText("UI_Text_Towing_cannotDriveWhileTowed") if not msg or msg == "UI_Text_Towing_cannotDriveWhileTowed" then return CannotDriveWhileTowedFallbackText end return msg end function TowBarMod.Hook.showCannotDriveWhileTowed(playerObj) if not playerObj then return end local playerNum = playerObj:getPlayerNum() local nowHours = getGameTime() and getGameTime():getWorldAgeHours() or nil local lastHours = TowBarMod.Hook.lastCannotDriveMessageAtByPlayer[playerNum] if nowHours and lastHours and (nowHours - lastHours) < CannotDriveMessageCooldownHours then return end TowBarMod.Hook.lastCannotDriveMessageAtByPlayer[playerNum] = nowHours or 0 -- Match the overhead-style skill/feedback text. HaloTextHelper.addBadText(playerObj, TowBarMod.Hook.getCannotDriveWhileTowedText()) end function TowBarMod.Hook.installStartEngineBlock() if not ISVehicleMenu or not ISVehicleMenu.onStartEngine then return end if TowBarMod.Hook._startEngineBlockInstalled then return end TowBarMod.Hook.defaultOnStartEngine = ISVehicleMenu.onStartEngine ISVehicleMenu.onStartEngine = function(playerObj) local vehicle = playerObj and playerObj:getVehicle() or nil if TowBarMod.Hook.isTowedByTowBar(vehicle) then TowBarMod.Hook.showCannotDriveWhileTowed(playerObj) return end TowBarMod.Hook.defaultOnStartEngine(playerObj) end TowBarMod.Hook._startEngineBlockInstalled = true end function TowBarMod.Hook.getVehicleByIdSafe(vehicleId) if getVehicleById then return getVehicleById(vehicleId) end local cell = getCell() if not cell then return nil end local vehicles = cell:getVehicles() if not vehicles then return nil end for i = 0, vehicles:size() - 1 do local vehicle = vehicles:get(i) if vehicle and vehicle:getId() == vehicleId then return vehicle end end return nil end function TowBarMod.Hook.shouldBlockDriverSeatForTowBar(playerObj, vehicle, seat) if not playerObj or not vehicle then return false end if seat ~= 0 then return false end return TowBarMod.Hook.isTowedByTowBar(vehicle) end function TowBarMod.Hook.hasPendingSeatSafetyAction(playerObj) if not playerObj or not ISTimedActionQueue then return false end return ISTimedActionQueue.hasActionType(playerObj, "ISSwitchVehicleSeat") or ISTimedActionQueue.hasActionType(playerObj, "ISExitVehicle") or ISTimedActionQueue.hasActionType(playerObj, "ISStopVehicle") end function TowBarMod.Hook.getBestSeatForDriverKick(playerObj, vehicle) if not playerObj or not vehicle then return nil end if ISVehicleMenu and ISVehicleMenu.getBestSwitchSeatExit then local best = ISVehicleMenu.getBestSwitchSeatExit(playerObj, vehicle, 0) if best and best > 0 and not vehicle:isSeatOccupied(best) and vehicle:canSwitchSeat(0, best) then return best end end for seat = 1, vehicle:getMaxPassengers() - 1 do if not vehicle:isSeatOccupied(seat) and vehicle:canSwitchSeat(0, seat) then return seat end end return nil end function TowBarMod.Hook.tryMoveOrExitTowedDriver(playerObj, vehicle) if not playerObj or not vehicle then return false end if not TowBarMod.Hook.isTowedByTowBar(vehicle) then return false end if not vehicle:isDriver(playerObj) and vehicle:getSeat(playerObj) ~= 0 then return false end if TowBarMod.Hook.hasPendingSeatSafetyAction(playerObj) then return false end local seatTo = TowBarMod.Hook.getBestSeatForDriverKick(playerObj, vehicle) if seatTo then if ISVehicleMenu and ISVehicleMenu.onSwitchSeat then ISVehicleMenu.onSwitchSeat(playerObj, seatTo) elseif ISSwitchVehicleSeat then ISTimedActionQueue.add(ISSwitchVehicleSeat:new(playerObj, seatTo)) end return true end if not vehicle:isStopped() then return false end if ISVehicleMenu and ISVehicleMenu.onExit then ISVehicleMenu.onExit(playerObj, 0) return true end if ISExitVehicle then ISTimedActionQueue.add(ISExitVehicle:new(playerObj)) return true end return false end function TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj) if not TowBarMod.Hook.hasTowBarTowData(vehicle) then return false end local vehicleId = vehicle:getId() local nowHours = getGameTime() and getGameTime():getWorldAgeHours() or nil local lastHours = TowBarMod.Hook.lastForcedReapplyAtByVehicle[vehicleId] if nowHours and lastHours and (nowHours - lastHours) < ForcedTowBarReapplyCooldownHours then return false end TowBarMod.Hook.lastForcedReapplyAtByVehicle[vehicleId] = nowHours or 0 TowBarMod.Hook.clearReapplied(vehicle) TowBarMod.Hook.queueTowBarReapply(vehicle) return TowBarMod.Hook.tryTowBarReapply(vehicle, playerObj) end function TowBarMod.Hook.installSeatEntryBlock() if not ISVehicleMenu or not ISVehicleMenu.onEnter or not ISVehicleMenu.onSwitchSeat then return end if TowBarMod.Hook._seatEntryBlockInstalled then return end TowBarMod.Hook.defaultOnEnter = ISVehicleMenu.onEnter TowBarMod.Hook.defaultOnEnter2 = ISVehicleMenu.onEnter2 TowBarMod.Hook.defaultOnSwitchSeat = ISVehicleMenu.onSwitchSeat ISVehicleMenu.onEnter = function(playerObj, vehicle, seat) if TowBarMod.Hook.shouldBlockDriverSeatForTowBar(playerObj, vehicle, seat) then TowBarMod.Hook.showCannotDriveWhileTowed(playerObj) TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj) TowBarMod.Hook.tryMoveOrExitTowedDriver(playerObj, vehicle) return end TowBarMod.Hook.defaultOnEnter(playerObj, vehicle, seat) end if TowBarMod.Hook.defaultOnEnter2 then ISVehicleMenu.onEnter2 = function(playerObj, vehicle, seat) if TowBarMod.Hook.shouldBlockDriverSeatForTowBar(playerObj, vehicle, seat) then TowBarMod.Hook.showCannotDriveWhileTowed(playerObj) TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj) TowBarMod.Hook.tryMoveOrExitTowedDriver(playerObj, vehicle) return end TowBarMod.Hook.defaultOnEnter2(playerObj, vehicle, seat) end end ISVehicleMenu.onSwitchSeat = function(playerObj, seatTo) local vehicle = playerObj and playerObj:getVehicle() or nil if TowBarMod.Hook.shouldBlockDriverSeatForTowBar(playerObj, vehicle, seatTo) then TowBarMod.Hook.showCannotDriveWhileTowed(playerObj) TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj) TowBarMod.Hook.tryMoveOrExitTowedDriver(playerObj, vehicle) return end TowBarMod.Hook.defaultOnSwitchSeat(playerObj, seatTo) end TowBarMod.Hook._seatEntryBlockInstalled = true end function TowBarMod.Hook.enforceTowedVehicleEngineOff(playerObj) if not playerObj or not instanceof(playerObj, "IsoPlayer") or not playerObj:isLocalPlayer() then return end local vehicle = playerObj:getVehicle() if not TowBarMod.Hook.isTowedByTowBar(vehicle) then return end if vehicle:isEngineRunning() or vehicle:isEngineStarted() or vehicle:isStarting() then vehicle:shutOff() end end function TowBarMod.Hook.enforceTowedVehicleSeatSafety(playerObj) if not playerObj or not instanceof(playerObj, "IsoPlayer") or not playerObj:isLocalPlayer() then return end local vehicle = playerObj:getVehicle() if not TowBarMod.Hook.isTowedByTowBar(vehicle) then return end TowBarMod.Hook.lastTowBarVehicleIdByPlayer[playerObj:getPlayerNum()] = vehicle:getId() if vehicle:isDriver(playerObj) or vehicle:getSeat(playerObj) == 0 then TowBarMod.Hook.showCannotDriveWhileTowed(playerObj) TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj) TowBarMod.Hook.tryMoveOrExitTowedDriver(playerObj, vehicle) end end function TowBarMod.Hook.handleTowBarSeatEvent(character) if not character or not instanceof(character, "IsoPlayer") or not character:isLocalPlayer() then return end local playerObj = character local vehicle = playerObj:getVehicle() if not TowBarMod.Hook.isTowedByTowBar(vehicle) then return end TowBarMod.Hook.lastTowBarVehicleIdByPlayer[playerObj:getPlayerNum()] = vehicle:getId() TowBarMod.Hook.forceTowBarReapply(vehicle, playerObj) TowBarMod.Hook.enforceTowedVehicleSeatSafety(playerObj) end function TowBarMod.Hook.OnEnterVehicle(character) TowBarMod.Hook.handleTowBarSeatEvent(character) end function TowBarMod.Hook.OnSwitchVehicleSeat(character) TowBarMod.Hook.handleTowBarSeatEvent(character) end function TowBarMod.Hook.OnExitVehicle(character) if not character or not instanceof(character, "IsoPlayer") or not character:isLocalPlayer() then return end local playerNum = character:getPlayerNum() local vehicleId = TowBarMod.Hook.lastTowBarVehicleIdByPlayer[playerNum] if not vehicleId then return end local vehicle = TowBarMod.Hook.getVehicleByIdSafe(vehicleId) if vehicle and TowBarMod.Hook.hasTowBarTowData(vehicle) then TowBarMod.Hook.forceTowBarReapply(vehicle, character) end end function TowBarMod.Hook.markReapplied(vehicle) if not vehicle then return end local vehicleId = vehicle:getId() TowBarMod.Hook.reappliedVehicleIds[vehicleId] = true TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil end function TowBarMod.Hook.clearReapplied(vehicle) if not vehicle then return end local vehicleId = vehicle:getId() TowBarMod.Hook.reappliedVehicleIds[vehicleId] = nil TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil end function TowBarMod.Hook.hasTowBarTowData(vehicle) if not vehicle then return false end local modData = vehicle:getModData() return modData and modData["isTowingByTowBar"] and modData["towed"] end function TowBarMod.Hook.queueTowBarReapply(vehicle) if not TowBarMod.Hook.hasTowBarTowData(vehicle) then return end local vehicleId = vehicle:getId() TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = true end function TowBarMod.Hook.tryTowBarReapply(vehicle, playerObj) if not TowBarMod.Hook.hasTowBarTowData(vehicle) then return false end local vehicleId = vehicle:getId() if TowBarMod.Hook.reappliedVehicleIds[vehicleId] then return true end local towingVehicle = vehicle:getVehicleTowedBy() if not towingVehicle then return false end local towingModData = towingVehicle:getModData() if not towingModData or towingModData["isTowingByTowBar"] ~= true then return false end playerObj = playerObj or getPlayer() if not playerObj then return false end local modData = vehicle:getModData() if not modData.towBarOriginalScriptName then modData.towBarOriginalScriptName = vehicle:getScriptName() vehicle:transmitModData() end local attachmentA = towingVehicle:getTowAttachmentSelf() or "trailer" local attachmentB = vehicle:getTowAttachmentSelf() or "trailerfront" -- Re-run the same rigid tow offset + fake-trailer flow used during normal attach. TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, vehicle, attachmentA, attachmentB) vehicle:setScriptName("notTowingA_Trailer") local args = { vehicleA = towingVehicle:getId(), vehicleB = vehicle:getId(), attachmentA = attachmentA, attachmentB = attachmentB } sendClientCommand(playerObj, "vehicle", "attachTrailer", args) ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, vehicle)) TowBarMod.Hook.markReapplied(vehicle) return true end function TowBarMod.Hook.reapplyTowBarPostLoad(vehicle) TowBarMod.Hook.queueTowBarReapply(vehicle) TowBarMod.Hook.tryTowBarReapply(vehicle, getPlayer()) end function TowBarMod.Hook.processPendingReapplies() TowBarMod.Hook.reapplyTickCounter = TowBarMod.Hook.reapplyTickCounter + 1 if TowBarMod.Hook.reapplyTickCounter < 15 then return end TowBarMod.Hook.reapplyTickCounter = 0 local playerObj = getPlayer() if not playerObj then return end for vehicleId, _ in pairs(TowBarMod.Hook.pendingReapplyVehicleIds) do local vehicle = TowBarMod.Hook.getVehicleByIdSafe(vehicleId) if vehicle then TowBarMod.Hook.tryTowBarReapply(vehicle, playerObj) end end end function TowBarMod.Hook.OnSpawnVehicle(vehicle) TowBarMod.Hook.reapplyTowBarPostLoad(vehicle) end function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end local towBarItem = playerObj:getInventory():getItemFromType("TowBar.TowBar") 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() local part = towedVehicle:getPartById("towbar") if part ~= nil then if towedVehicle:getScript():getModelScale() >= 1.5 and towedVehicle:getScript():getModelScale() <= 2 then local z = towedVehicle:getScript():getPhysicsChassisShape():z()/2 - 0.1 part:setModelVisible("towbar" .. math.floor((z*2/3-1)*10), true) end end towedVehicle:doDamageOverlay() -- Fake a trailer script so the base "attachTrailer" command creates rigid point-constraint towing. 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)) TowBarMod.Hook.markReapplied(towedVehicle) end function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle) if not towedVehicle then return end local towedModData = towedVehicle:getModData() if towedModData.towBarOriginalScriptName then towedVehicle:setScriptName(towedModData.towBarOriginalScriptName) end towedVehicle:setMass(TowBarTowMass) towedVehicle:setBrakingForce(0) towedVehicle:constraintChanged() towedVehicle:updateTotalMass() end function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehicle) if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end local item = playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar") 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.performDeattachTowBar(playerObj, towingVehicle, towedVehicle) TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle) local args = { vehicle = towedVehicle:getId() } sendClientCommand(playerObj, "towbar", "detachConstraint", 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 }) towingVehicle:getModData()["isTowingByTowBar"] = false towedModData["isTowingByTowBar"] = false towedModData["towed"] = false towedModData.towBarOriginalScriptName = nil towedModData.towBarOriginalMass = nil towedModData.towBarOriginalBrakingForce = nil towingVehicle:transmitModData() towedVehicle:transmitModData() TowBarMod.Hook.clearReapplied(towedVehicle) TowBarMod.Hook.lastForcedReapplyAtByVehicle[towedVehicle:getId()] = nil local part = towedVehicle:getPartById("towbar") if part ~= nil then for j=0, 23 do part:setModelVisible("towbar" .. j, false) end end towedVehicle:doDamageOverlay() end function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle) local towingVehicle = vehicle local towedVehicle = vehicle:getVehicleTowing() if 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.performDeattachTowBar, towingVehicle, towedVehicle )) end TowBarMod.Hook.installStartEngineBlock() TowBarMod.Hook.installSeatEntryBlock() Events.OnGameStart.Add(TowBarMod.Hook.installStartEngineBlock) Events.OnGameStart.Add(TowBarMod.Hook.installSeatEntryBlock) Events.OnPlayerUpdate.Add(TowBarMod.Hook.enforceTowedVehicleEngineOff) Events.OnPlayerUpdate.Add(TowBarMod.Hook.enforceTowedVehicleSeatSafety) Events.OnSpawnVehicleEnd.Add(TowBarMod.Hook.OnSpawnVehicle) Events.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle) Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat) Events.OnExitVehicle.Add(TowBarMod.Hook.OnExitVehicle) Events.OnTick.Add(TowBarMod.Hook.processPendingReapplies)