diff --git a/42.18/media/lua/client/TowBar/Config.lua b/42.18/media/lua/client/TowBar/Config.lua index f956d1f..73e39fd 100644 --- a/42.18/media/lua/client/TowBar/Config.lua +++ b/42.18/media/lua/client/TowBar/Config.lua @@ -3,6 +3,8 @@ if not TowBarMod.Config then TowBarMod.Config = {} end TowBarMod.Config.lowLevelAnimation = "RemoveGrass" TowBarMod.Config.rigidTowbarDistance = 1.0 +TowBarMod.Config.towAttachmentExteriorPadding = 0.25 +TowBarMod.Config.towedVehicleRollingMass = 75 TowBarMod.Config.devMode = false TowBarMod.Config.vanillaTowbarModelScaleMin = 1.5 TowBarMod.Config.vanillaTowbarModelScaleMax = 2.0 diff --git a/42.18/media/lua/client/TowBar/TowingHooking.lua b/42.18/media/lua/client/TowBar/TowingHooking.lua index 715e803..0827763 100644 --- a/42.18/media/lua/client/TowBar/TowingHooking.lua +++ b/42.18/media/lua/client/TowBar/TowingHooking.lua @@ -1,11 +1,88 @@ if not TowBarMod then TowBarMod = {} end if not TowBarMod.Hook then TowBarMod.Hook = {} end -local TowBarTowMass = 200 +local DefaultTowBarTowMass = 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 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", "isParkingBrake") + storeOriginalVehicleCall(vehicle, modData, "towBarOriginalHandbrake", "isHandbrake") + + local configuredTowMass = TowBarMod.Config and tonumber(TowBarMod.Config.towedVehicleRollingMass) + vehicle:setMass(configuredTowMass or DefaultTowBarTowMass) + vehicle:setBrakingForce(0) + + tryVehicleCall(vehicle, "setParkingBrakeOn", false) + tryVehicleCall(vehicle, "setParkingBrake", false) + tryVehicleCall(vehicle, "setHandbrake", false) + tryVehicleCall(vehicle, "setBrake", false) + tryVehicleCall(vehicle, "setBraking", false) + + vehicle:constraintChanged() + vehicle:updateTotalMass() +end + +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 isTowBarTowPair(towingVehicle, towedVehicle) if not towingVehicle or not towedVehicle then return false end @@ -269,12 +346,7 @@ local function reattachTowBarPair(playerObj, towingVehicle, towedVehicle, requir 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 + applyFreeRollingTowState(towedVehicle) towingModData["isTowingByTowBar"] = true towedModData["isTowingByTowBar"] = true @@ -376,10 +448,7 @@ function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, retriesLef end end - towedVehicle:setMass(TowBarTowMass) - towedVehicle:setBrakingForce(0) - towedVehicle:constraintChanged() - towedVehicle:updateTotalMass() + 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. @@ -402,8 +471,7 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic local towedModData = towedVehicle:getModData() towedModData.towBarOriginalScriptName = towedVehicle:getScriptName() - towedModData.towBarOriginalMass = towedVehicle:getMass() - towedModData.towBarOriginalBrakingForce = towedVehicle:getBrakingForce() + applyFreeRollingTowState(towedVehicle) towingModData["isTowingByTowBar"] = true towedModData["isTowingByTowBar"] = true @@ -438,14 +506,7 @@ function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehic 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() + restoreFreeRollingTowState(towedVehicle, towedModData) sendClientCommand(playerObj, "towbar", "giveTowBar", { equipPrimary = true }) @@ -456,6 +517,9 @@ function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehic towedModData.towBarOriginalScriptName = nil towedModData.towBarOriginalMass = nil towedModData.towBarOriginalBrakingForce = nil + towedModData.towBarOriginalParkingBrakeOn = nil + towedModData.towBarOriginalParkingBrake = nil + towedModData.towBarOriginalHandbrake = nil towingVehicle:transmitModData() towedVehicle:transmitModData() @@ -526,6 +590,27 @@ local function forEachCollectionItem(collection, callback) 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.OnEnterVehicle(character) tryAutoReattachFromCharacter(character) end @@ -716,3 +801,4 @@ if Events.OnGameStart then end Events.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle) Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat) +Events.OnTick.Add(keepTowBarVehiclesFreeRolling) diff --git a/42.18/media/lua/client/TowBar/TowingUtils.lua b/42.18/media/lua/client/TowBar/TowingUtils.lua index d757eef..52ba980 100644 --- a/42.18/media/lua/client/TowBar/TowingUtils.lua +++ b/42.18/media/lua/client/TowBar/TowingUtils.lua @@ -23,6 +23,53 @@ local function computeAttachmentHeight(vehicle) return -0.5 end +local function getVehicleHalfLength(script) + if not script then return nil end + + local ok, shape = pcall(function() + return script:getPhysicsChassisShape() + end) + if ok and shape then + local zOk, z = pcall(function() + return shape:z() + end) + z = zOk and tonumber(z) or nil + if z and z > 0 then return z / 2 end + end + + ok, shape = pcall(function() + return script:getExtents() + end) + if ok and shape then + local zOk, z = pcall(function() + return shape:z() + end) + z = zOk and tonumber(z) or nil + if z and z > 0 then return z / 2 end + end + + return nil +end + +local function getExteriorAttachmentZ(script, attachmentId, originalZ, extraDistance) + local halfLength = getVehicleHalfLength(script) + if halfLength == nil then return originalZ end + + local configuredPadding = TowBarMod.Config and tonumber(TowBarMod.Config.towAttachmentExteriorPadding) + local padding = configuredPadding or 0.25 + local direction = originalZ >= 0 and 1 or -1 + if originalZ == 0 and attachmentId == "trailer" then + direction = -1 + end + local exteriorZ = direction * (halfLength + padding + (extraDistance or 0)) + + if math.abs(originalZ) > math.abs(exteriorZ) then + return originalZ + end + + return exteriorZ +end + function TowBarMod.Utils.isTrailer(vehicle) return string.match(string.lower(vehicle:getScript():getName()), "trailer") end @@ -93,8 +140,12 @@ function TowBarMod.Utils.getHookTypeVariants(vehicleA, vehicleB, hasTowBar) end function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB) - local towingAttachment = towingVehicle:getScript():getAttachmentById(attachmentA) - local towedAttachment = towedVehicle:getScript():getAttachmentById(attachmentB) + local towingScript = towingVehicle:getScript() + local towedScript = towedVehicle:getScript() + if towingScript == nil or towedScript == nil then return end + + local towingAttachment = towingScript:getAttachmentById(attachmentA) + local towedAttachment = towedScript:getAttachmentById(attachmentB) if towingAttachment == nil or towedAttachment == nil then return end towingAttachment:setUpdateConstraint(false) @@ -109,12 +160,16 @@ function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicl -- Store and update the towing vehicle's attachment Y. local towingModData = towingVehicle:getModData() - if towingModData["towBarOriginalTowingOffsetY"] == nil then - towingModData["towBarOriginalTowingOffsetY"] = towingAttachment:getOffset():y() + local towingOffset = towingAttachment:getOffset() + if towingModData["towBarOriginalTowingAttachmentId"] ~= attachmentA + or towingModData["towBarOriginalTowingOffsetX"] == nil + or towingModData["towBarOriginalTowingOffsetY"] == nil + or towingModData["towBarOriginalTowingOffsetZ"] == nil then + towingModData["towBarOriginalTowingOffsetX"] = towingOffset:x() + towingModData["towBarOriginalTowingOffsetY"] = towingOffset:y() + towingModData["towBarOriginalTowingOffsetZ"] = towingOffset:z() towingModData["towBarOriginalTowingAttachmentId"] = attachmentA end - local towingOffset = towingAttachment:getOffset() - towingAttachment:getOffset():set(towingOffset:x(), towingHeight, towingOffset:z()) local towedModData = towedVehicle:getModData() local spacingDistance = 1.0 @@ -122,6 +177,11 @@ function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicl spacingDistance = tonumber(TowBarMod.Config.rigidTowbarDistance) end + local towingBaseX = tonumber(towingModData["towBarOriginalTowingOffsetX"]) or towingOffset:x() + local towingBaseZ = tonumber(towingModData["towBarOriginalTowingOffsetZ"]) or towingOffset:z() + local towingExteriorZ = getExteriorAttachmentZ(towingScript, attachmentA, towingBaseZ, 0) + towingAttachment:getOffset():set(towingBaseX, towingHeight, towingExteriorZ) + local offset = towedAttachment:getOffset() local storedBaseX = tonumber(towedModData["towBarBaseAttachmentOffsetX"]) local storedBaseY = tonumber(towedModData["towBarBaseAttachmentOffsetY"]) @@ -140,9 +200,9 @@ function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicl towedModData["towBarBaseAttachmentOffsetZ"] = baseZ end - local zDirection = baseZ >= 0 and 1 or -1 - local zShift = zDirection * spacingDistance - towedAttachment:getOffset():set(baseX, towedHeight, baseZ + zShift) + local towedExteriorZ = getExteriorAttachmentZ(towedScript, attachmentB, baseZ, spacingDistance) + local zShift = towedExteriorZ - baseZ + towedAttachment:getOffset():set(baseX, towedHeight, towedExteriorZ) towedModData["isChangedTowedAttachment"] = true towedModData["towBarChangedAttachmentId"] = attachmentB @@ -161,14 +221,20 @@ function TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVe local zOffset = (towingAttachmentId == "trailer") and -1 or 1 towingAttachment:setZOffset(zOffset) - -- Restore the original Y offset that was overridden by dynamic height. + -- Restore the original offset that was overridden for rigid tow spacing. + local originalX = tonumber(towingModData["towBarOriginalTowingOffsetX"]) local originalY = tonumber(towingModData["towBarOriginalTowingOffsetY"]) - if originalY ~= nil then + local originalZ = tonumber(towingModData["towBarOriginalTowingOffsetZ"]) + if originalX ~= nil and originalY ~= nil and originalZ ~= nil then + towingAttachment:getOffset():set(originalX, originalY, originalZ) + elseif originalY ~= nil then local off = towingAttachment:getOffset() towingAttachment:getOffset():set(off:x(), originalY, off:z()) end end + towingModData["towBarOriginalTowingOffsetX"] = nil towingModData["towBarOriginalTowingOffsetY"] = nil + towingModData["towBarOriginalTowingOffsetZ"] = nil towingModData["towBarOriginalTowingAttachmentId"] = nil towingVehicle:transmitModData() @@ -249,4 +315,3 @@ local function fixTowAttachmentsForOtherVehicleMods() end Events.OnGameBoot.Add(fixTowAttachmentsForOtherVehicleMods) -