Working MP

This commit is contained in:
2026-02-07 16:16:23 -05:00
parent c5b4d31f3d
commit 36b6c697e3
8 changed files with 1756 additions and 466 deletions

View File

@@ -1,397 +1,66 @@
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 {}
local AutoReattachCooldownHours = 1 / 7200 -- 0.5 seconds
TowBarMod.Hook.lastAutoReattachAtByVehicle = TowBarMod.Hook.lastAutoReattachAtByVehicle 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
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
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)
local function setTowBarModelVisible(vehicle, isVisible)
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
local part = vehicle:getPartById("towbar")
if part == nil then return 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
if vehicle then
TowBarMod.Hook.pendingReapplyVehicleIds[vehicle:getId()] = nil
end
return false
for j = 0, 23 do
part:setModelVisible("towbar" .. j, false)
end
local vehicleId = vehicle:getId()
if TowBarMod.Hook.reappliedVehicleIds[vehicleId] then
TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil
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
local modData = vehicle:getModData()
if not modData.towBarOriginalScriptName then
modData.towBarOriginalScriptName = vehicle:getScriptName()
vehicle:transmitModData()
end
playerObj = playerObj or getPlayer()
if not playerObj then return false 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)
if not TowBarMod.Hook.hasTowBarTowData(vehicle) then
if not isVisible then
vehicle:doDamageOverlay()
return
end
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
local script = vehicle:getScript()
if not script then
vehicle:doDamageOverlay()
return
end
TowBarMod.Hook.reapplyTickCounter = 0
local playerObj = getPlayer()
if not playerObj then return end
local resolvedVehicleIds = {}
for vehicleId, _ in pairs(TowBarMod.Hook.pendingReapplyVehicleIds) do
local vehicle = TowBarMod.Hook.getVehicleByIdSafe(vehicleId)
if vehicle and (TowBarMod.Hook.tryTowBarReapply(vehicle, playerObj) or not TowBarMod.Hook.hasTowBarTowData(vehicle)) then
table.insert(resolvedVehicleIds, vehicleId)
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
for _, vehicleId in ipairs(resolvedVehicleIds) do
TowBarMod.Hook.pendingReapplyVehicleIds[vehicleId] = nil
end
vehicle:doDamageOverlay()
end
function TowBarMod.Hook.OnSpawnVehicle(vehicle)
TowBarMod.Hook.reapplyTowBarPostLoad(vehicle)
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 = playerObj:getInventory():getItemFromType("TowBar.TowBar")
local towBarItem = getTowBarItem(playerObj)
if towBarItem ~= nil then
sendClientCommand(playerObj, "towbar", "consumeTowBar", { itemId = towBarItem:getID() })
end
@@ -401,6 +70,7 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
towedModData.towBarOriginalScriptName = towedVehicle:getScriptName()
towedModData.towBarOriginalMass = towedVehicle:getMass()
towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
@@ -411,44 +81,149 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
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
setTowBarModelVisible(towedVehicle, true)
towedVehicle:doDamageOverlay()
-- Fake a trailer script so the base "attachTrailer" command creates rigid point-constraint towing.
-- 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 }
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
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
towedVehicle:setMass(TowBarTowMass)
towedVehicle:setBrakingForce(0)
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 towingModData["isTowingByTowBar"] then return end
if not towedModData["isTowingByTowBar"] or not towedModData["towed"] 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 towingVehicle = playerObj:getVehicle()
if not towingVehicle then return end
if not towingVehicle:isDriver(playerObj) then return end
if not towingVehicle:getVehicleTowing() then return end
local modData = towingVehicle:getModData()
if not modData or not modData["isTowingByTowBar"] then return end
local vehicleId = towingVehicle:getId()
local nowHours = getGameTime() and getGameTime():getWorldAgeHours() or 0
local lastHours = TowBarMod.Hook.lastAutoReattachAtByVehicle[vehicleId]
if lastHours and (nowHours - lastHours) < AutoReattachCooldownHours then
return
end
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 = playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar")
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)
@@ -482,52 +257,10 @@ function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehi
))
end
function TowBarMod.Hook.performDeattachTowBar(playerObj, towingVehicle, towedVehicle)
TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
-- Detach from the towing side to mirror vanilla trailer detach behavior.
local args = { vehicle = towingVehicle:getId() }
sendClientCommand(playerObj, "vehicle", "detachTrailer", 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
local towedVehicle = vehicle and vehicle:getVehicleTowing() or nil
if vehicle and vehicle:getVehicleTowedBy() then
towingVehicle = vehicle:getVehicleTowedBy()
towedVehicle = vehicle
end
@@ -562,22 +295,32 @@ function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle)
playerObj,
300,
TowBarMod.Config.lowLevelAnimation,
TowBarMod.Hook.performDeattachTowBar,
TowBarMod.Hook.performDetachTowBar,
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)
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)
Events.OnExitVehicle.Add(TowBarMod.Hook.OnExitVehicle)
Events.OnTick.Add(TowBarMod.Hook.processPendingReapplies)