Files
Towbar/42.13/media/lua/client/TowBar/TowingHooking.lua

585 lines
21 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 TowbarVariantSize = 24
local TowbarNormalStart = 0
local TowbarLargeStart = 24
local TowbarMaxIndex = TowbarVariantSize - 1
local function getVehicleModelScale(script)
if not script then return nil end
local ok, result = pcall(function()
return script:getModelScale()
end)
if ok and type(result) == "number" then
return result
end
ok, result = pcall(function()
local model = script:getModel()
if model then
return model:getScale()
end
return nil
end)
if ok and type(result) == "number" then
return result
end
return nil
end
local function shouldUseLargeTowbarModel(script)
local modelScale = getVehicleModelScale(script)
if modelScale == nil then
return false
end
local configuredThreshold = TowBarMod.Config and tonumber(TowBarMod.Config.largeTowbarModelScaleThreshold)
local threshold = configuredThreshold or 1.2
return modelScale < threshold
end
local function getTowbarModelSlot(script)
local chassisZ = script:getPhysicsChassisShape():z()
local index = 0
if chassisZ > 3.0 then
local halfZ = chassisZ / 2
index = math.floor((halfZ - 1.0) * 16 - 1)
end
index = math.max(0, math.min(TowbarMaxIndex, index))
local slotStart = shouldUseLargeTowbarModel(script) and TowbarLargeStart or TowbarNormalStart
return slotStart + index, index, slotStart
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, (TowbarVariantSize * 2) - 1 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 slot = getTowbarModelSlot(script)
part:setModelVisible("towbar" .. slot, true)
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()
-- 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 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
---------------------------------------------------------------------------
--- Dev / debug helpers
---------------------------------------------------------------------------
function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle)
if not vehicle then return end
local part = vehicle:getPartById("towbar")
if part == nil then
print("[TowBar DEV] No 'towbar' 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 modelScale = script and getVehicleModelScale(script) or nil
local slot, index, slotStart = 0, 0, TowbarNormalStart
if script then
slot, index, slotStart = getTowbarModelSlot(script)
end
print("[TowBar DEV] Vehicle: " .. tostring(vehicle:getScriptName()))
print("[TowBar DEV] chassisShape.z = " .. tostring(chassisZ) .. ", half = " .. tostring(halfZ))
print("[TowBar DEV] modelScale = " .. tostring(modelScale) .. ", largeModel = " .. tostring(slotStart == TowbarLargeStart))
print("[TowBar DEV] Formula picks index = " .. tostring(index) .. " (towbar" .. tostring(slot) .. " at Z offset " .. tostring(1.0 + index * 0.1) .. ") [chassisZ > 3.0: " .. tostring(chassisZ > 3.0) .. "]")
print("[TowBar DEV] Showing ALL 48 towbar models (towbar0..towbar47)")
for j = 0, (TowbarVariantSize * 2) - 1 do
part:setModelVisible("towbar" .. j, true)
end
vehicle:doDamageOverlay()
end
function TowBarMod.Hook.devHideAllTowbarModels(playerObj, vehicle)
if not vehicle then return end
local part = vehicle:getPartById("towbar")
if part == nil then
print("[TowBar DEV] No 'towbar' part found on vehicle " .. tostring(vehicle:getScriptName()))
return
end
print("[TowBar DEV] Hiding ALL towbar models on " .. tostring(vehicle:getScriptName()))
for j = 0, (TowbarVariantSize * 2) - 1 do
part:setModelVisible("towbar" .. j, false)
end
vehicle:doDamageOverlay()
end
function TowBarMod.Hook.devShowSingleTowbar(playerObj, vehicle, index)
if not vehicle then return end
local part = vehicle:getPartById("towbar")
if part == nil then
print("[TowBar DEV] No 'towbar' part found on vehicle " .. tostring(vehicle:getScriptName()))
return
end
for j = 0, (TowbarVariantSize * 2) - 1 do
part:setModelVisible("towbar" .. j, false)
end
print("[TowBar DEV] Showing only towbar" .. tostring(index) .. " (Z offset " .. tostring(1.0 + index * 0.1) .. ") on " .. tostring(vehicle:getScriptName()))
part:setModelVisible("towbar" .. index, true)
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)