670 lines
25 KiB
Lua
670 lines
25 KiB
Lua
if not TowBarMod then TowBarMod = {} end
|
|
if not TowBarMod.Hook then TowBarMod.Hook = {} end
|
|
|
|
require("TowBar/VehicleCompatibility")
|
|
|
|
local DefaultTowBarTowMass = 200
|
|
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", "getParkingBrake")
|
|
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
|
|
|
|
-- Match the working wrecker path. Recalculating total mass here would
|
|
-- immediately replace the temporary towing mass in multiplayer; creating
|
|
-- the rigid constraint below notifies Bullet of the changed vehicle state.
|
|
end
|
|
|
|
TowBarMod.Hook.applyFreeRollingTowState = applyFreeRollingTowState
|
|
|
|
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 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
|
|
-- SP and MP now share the same authoritative attach lifecycle.
|
|
sendClientCommand(playerObj, "towbar", "attachTowBar", 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 VanillaScaleMin = 1.5
|
|
local VanillaScaleMax = 2.0
|
|
|
|
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 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()
|
|
return model and model:getScale() or nil
|
|
end)
|
|
if ok and type(result) == "number" then return result end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function isVanillaScale(script)
|
|
local modelScale = getVehicleModelScale(script)
|
|
if modelScale == nil then return true end
|
|
|
|
local configuredMin = TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMin)
|
|
local configuredMax = TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMax)
|
|
return modelScale >= (configuredMin or VanillaScaleMin)
|
|
and modelScale <= (configuredMax or VanillaScaleMax)
|
|
end
|
|
|
|
local function getTowbarIndexVanilla(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 zOk, shapeZ = pcall(function() return shape:z() end)
|
|
if not zOk or type(shapeZ) ~= "number" then return nil end
|
|
|
|
local z = shapeZ / 2 - 0.1
|
|
local index = math.floor((z * 2 / 3 - 1) * 10)
|
|
return math.max(0, math.min(TowbarMaxIndex, index))
|
|
end
|
|
|
|
local function getTowbarIndexSmallScale(script)
|
|
if not script then return nil end
|
|
|
|
local maxAbsTowZ = nil
|
|
local trailer = script:getAttachmentById("trailer")
|
|
if trailer then maxAbsTowZ = math.abs(trailer:getOffset():z()) end
|
|
local trailerFront = script:getAttachmentById("trailerfront")
|
|
if trailerFront then
|
|
local frontAbsZ = math.abs(trailerFront:getOffset():z())
|
|
if not maxAbsTowZ or frontAbsZ > maxAbsTowZ then maxAbsTowZ = frontAbsZ end
|
|
end
|
|
|
|
if maxAbsTowZ == nil then return nil end
|
|
local index = math.floor((maxAbsTowZ + 0.1 - 1.0) * 10)
|
|
return math.max(0, math.min(TowbarMaxIndex, index))
|
|
end
|
|
|
|
local function getLegacyTowbarModelSlot(script)
|
|
local useNormalPart = isVanillaScale(script)
|
|
local index = getTowbarIndexVanilla(script)
|
|
if not useNormalPart then
|
|
index = getTowbarIndexSmallScale(script) or index
|
|
if index == nil then
|
|
local offset = TowBarMod.Config and tonumber(TowBarMod.Config.smallScaleTowbarIndexOffset) or 2
|
|
index = math.max(0, math.min(TowbarMaxIndex, offset))
|
|
end
|
|
end
|
|
return index, useNormalPart
|
|
end
|
|
|
|
local function setTowBarModelVisible(vehicle, isVisible)
|
|
if not vehicle then return end
|
|
|
|
local normalPart = vehicle:getPartById("towbar")
|
|
local largePart = vehicle:getPartById("towbarLarge")
|
|
local ki5Part = vehicle:getPartById("towbarKI5")
|
|
if normalPart == nil and largePart == nil and ki5Part == 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
|
|
if ki5Part then ki5Part: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 isKi5 = TowBarMod.Compatibility.isKi5Vehicle(vehicle)
|
|
local index, useNormalPart
|
|
if isKi5 then
|
|
index = getTowbarModelSlot(script)
|
|
else
|
|
index, useNormalPart = getLegacyTowbarModelSlot(script)
|
|
end
|
|
|
|
local part = isKi5 and ki5Part or (useNormalPart and normalPart or largePart)
|
|
if part == nil and not isKi5 then part = normalPart or largePart end
|
|
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 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
|
|
|
|
function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, knownTowingVehicle)
|
|
if not towedVehicle then return end
|
|
|
|
local towedModData = towedVehicle:getModData()
|
|
if not isActiveTowBarTowedVehicle(towedVehicle, towedModData) then return end
|
|
|
|
local towingVehicle = knownTowingVehicle or towedVehicle:getVehicleTowedBy()
|
|
if not towingVehicle then return end
|
|
|
|
-- The rigid primitive passes the authoritative towing vehicle because MP
|
|
-- reciprocal getters can lag behind accepted local constraint submission.
|
|
if towedModData and towedModData.towBarOriginalScriptName
|
|
and towedVehicle:getScriptName() ~= towedModData.towBarOriginalScriptName then
|
|
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, towedModData.towBarOriginalScriptName)
|
|
end
|
|
|
|
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
|
|
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 then return end
|
|
playerObj:setPrimaryHandItem(nil)
|
|
|
|
local args = {
|
|
vehicleA = towingVehicle:getId(),
|
|
vehicleB = towedVehicle:getId(),
|
|
attachmentA = attachmentA,
|
|
attachmentB = attachmentB,
|
|
itemId = towBarItem and towBarItem:getID() or nil
|
|
}
|
|
sendTowAttachCommand(playerObj, args)
|
|
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()
|
|
|
|
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)
|
|
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.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)
|
|
-- Server persistence snapshots own attach recovery after vehicle streaming.
|
|
end
|
|
|
|
function TowBarMod.Hook.OnGameStart()
|
|
-- Server persistence broadcasts the same attach snapshot used by a fresh pair.
|
|
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")
|
|
local ki5Part = vehicle:getPartById("towbarKI5")
|
|
if normalPart == nil and largePart == nil and ki5Part == nil then
|
|
print("[TowBar DEV] No towbar model 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 all 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
|
|
if ki5Part then ki5Part: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")
|
|
local ki5Part = vehicle:getPartById("towbarKI5")
|
|
if normalPart == nil and largePart == nil and ki5Part == nil then
|
|
print("[TowBar DEV] No towbar model 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
|
|
if ki5Part then ki5Part: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")
|
|
local ki5Part = vehicle:getPartById("towbarKI5")
|
|
if normalPart == nil and largePart == nil and ki5Part == nil then
|
|
print("[TowBar DEV] No towbar model part found on vehicle " .. tostring(vehicle:getScriptName()))
|
|
return
|
|
end
|
|
|
|
local localIndex = math.max(0, math.min(TowbarMaxIndex, index % TowbarVariantSize))
|
|
local selectedPartId = "towbar"
|
|
if index >= TowbarVariantSize * 2 then
|
|
selectedPartId = "towbarKI5"
|
|
elseif index >= TowbarVariantSize then
|
|
selectedPartId = "towbarLarge"
|
|
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
|
|
if ki5Part then ki5Part:setModelVisible("towbar" .. j, false) end
|
|
end
|
|
|
|
local part = selectedPartId == "towbarKI5" and ki5Part
|
|
or (selectedPartId == "towbarLarge" and largePart or normalPart)
|
|
if part == nil then
|
|
part = normalPart or largePart or ki5Part
|
|
end
|
|
print("[TowBar DEV] Showing only towbar" .. tostring(localIndex) .. " on part " .. selectedPartId .. " (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.OnTick.Add(keepTowBarVehiclesFreeRolling)
|