Files
Towbar/42.13/media/lua/client/TowBar/TowingHooking.lua
2026-02-07 16:34:43 -05:00

353 lines
14 KiB
Lua

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)