42.20 and Towtrucks

This commit is contained in:
2026-08-13 18:59:54 -04:00
parent a6ba4877d0
commit e403323017
33 changed files with 2333 additions and 142 deletions
-2
View File
@@ -6,5 +6,3 @@ 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
@@ -4,6 +4,23 @@ if not TowBarMod then TowBarMod = {} end
TowBarMod.Sync = TowBarMod.Sync or {}
if TowBarMod.Sync._towSyncClientLoaded then return end
TowBarMod.Sync._towSyncClientLoaded = true
TowBarMod.Sync.appliedPairs = TowBarMod.Sync.appliedPairs or {}
local function pairKeyContainsVehicle(key, vehicleId)
local id = tostring(vehicleId)
return string.sub(key, 1, #id + 1) == id .. ":"
or string.sub(key, -(#id + 1)) == ":" .. id
end
local function clearAppliedPairForVehicle(vehicle)
if not vehicle then return end
local vehicleId = vehicle:getId()
for key in pairs(TowBarMod.Sync.appliedPairs) do
if pairKeyContainsVehicle(key, vehicleId) then
TowBarMod.Sync.appliedPairs[key] = nil
end
end
end
local function resolveVehicle(id)
if not id then return nil end
@@ -89,6 +106,8 @@ local function reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
end
end
local breakTowBarPair
local function applyAttachSync(args)
if not args then return end
@@ -102,8 +121,25 @@ local function applyAttachSync(args)
return
end
if not isLinked(vehicleA, vehicleB) then
vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB)
local key = tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())
if TowBarMod.Sync.appliedPairs[key] and not isLinked(vehicleA, vehicleB) then
-- Vehicle streaming can remove the native constraint while the Lua
-- module remains loaded. Allow the authoritative recovery sync to
-- rebuild local physics for the returning pair.
TowBarMod.Sync.appliedPairs[key] = nil
end
if not TowBarMod.Sync.appliedPairs[key] then
-- The server initially creates a normal Build 42 tow relation. It may
-- also restore one from the save. Replace that native rope exactly
-- once with this mod's rigid fake-trailer constraint.
breakTowBarPair(vehicleA, vehicleB)
if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicleB, "notTowingA_Trailer")
end
-- The final true keeps this a local physics rebuild. The server owns
-- the persisted logical relation and must not receive a detach/attach race.
vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB, true)
TowBarMod.Sync.appliedPairs[key] = true
end
reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
@@ -130,18 +166,17 @@ local function hasConflictingTowLink(vehicle, expectedOther)
and modData["towBarTowingVehicleId"] ~= expectedOtherId)
end
local function breakTowBarPair(vehicleA, vehicleB)
breakTowBarPair = function(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return end
local vehicleAReferencesB = vehicleA:getVehicleTowing() == vehicleB
or vehicleA:getVehicleTowedBy() == vehicleB
local vehicleBReferencesA = vehicleB:getVehicleTowing() == vehicleA
or vehicleB:getVehicleTowedBy() == vehicleA
if vehicleBReferencesA then
vehicleB:breakConstraint(true, true)
end
if vehicleAReferencesB then
vehicleA:breakConstraint(true, true)
elseif vehicleBReferencesA then
vehicleB:breakConstraint(true, true)
end
end
@@ -154,6 +189,7 @@ local function applyDetachSync(args)
return
end
breakTowBarPair(vehicleA, vehicleB)
TowBarMod.Sync.appliedPairs[tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())] = nil
if TowBarMod.Hook and TowBarMod.Hook.cleanupDetachedTowBar then
pcall(TowBarMod.Hook.cleanupDetachedTowBar, vehicleA, vehicleB)
@@ -170,4 +206,10 @@ local function onServerCommand(module, command, args)
end
end
TowBarMod.Sync.applyAttachSync = applyAttachSync
TowBarMod.Sync.applyDetachSync = applyDetachSync
Events.OnServerCommand.Add(onServerCommand)
if Events.OnSpawnVehicleEnd then
Events.OnSpawnVehicleEnd.Add(clearAppliedPairForVehicle)
end
+50 -57
View File
@@ -128,61 +128,54 @@ local function sendTowAttachCommand(playerObj, args)
end
local TowbarVariantSize = 24
local TowbarNormalStart = 0
local TowbarLargeStart = 24
local TowbarMaxIndex = TowbarVariantSize - 1
local VanillaScaleMin = 1.5
local VanillaScaleMax = 2.0
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 function getVehicleModelScale(script)
local function getTowbarFrontEdgeZ(script)
if not script then return nil end
local ok, result = pcall(function()
return script:getModelScale()
local ok, shape = pcall(function()
return script:getPhysicsChassisShape()
end)
if ok and type(result) == "number" then
return result
end
if not ok or not shape then return nil end
ok, result = pcall(function()
local model = script:getModel()
if model then
return model:getScale()
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 ok and type(result) == "number" then
return result
end
if not centerOk or not center then return nil end
return nil
end
local zOk, centerZ = pcall(function()
return center:z()
end)
if not zOk or type(centerZ) ~= "number" then 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)
local minScale = configuredMin or VanillaScaleMin
local maxScale = configuredMax or VanillaScaleMax
return modelScale >= minScale and modelScale <= maxScale
end
local function getTowbarIndexVanilla(script)
local z = script:getPhysicsChassisShape():z() / 2 - 0.1
local index = math.floor((z * 2 / 3 - 1) * 10)
return math.max(0, math.min(TowbarMaxIndex, index))
return centerZ + shapeZ / 2
end
local function getTowbarModelSlot(script)
if not isVanillaScale(script) then
-- KI5/small-scale vehicles use the normal towbar0 mesh at Z=1.
return 0
end
return getTowbarIndexVanilla(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 setTowBarModelVisible(vehicle, isVisible)
@@ -210,7 +203,7 @@ local function setTowBarModelVisible(vehicle, isVisible)
local index = getTowbarModelSlot(script)
local part = normalPart
if part then
if part and index ~= nil then
part:setModelVisible("towbar" .. index, true)
end
@@ -409,14 +402,10 @@ local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft)
if towingVehicle then
-- Apply rigid spacing as soon as the tow link exists to avoid a visible
-- bumper-to-bumper snap while waiting for reattach recovery.
-- bumper-to-bumper snap. Server/SP reconciliation owns recovery and
-- never refunds or consumes an item while loading a save.
TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
end
if localPlayer and towingVehicle then
if reattachTowBarPairAfterCleanDetach(localPlayer, towingVehicle, vehicle, false) then
return
end
return
end
if localPlayer and retries > 0 then
@@ -425,9 +414,8 @@ local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft)
return
end
-- Fallback: keep original post-attach restoration behavior.
setTowBarModelVisible(vehicle, true)
TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
-- Leave reciprocal saved state untouched. The authoritative audit will
-- reconnect it when both vehicle chunks are loaded.
end
function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, retriesLeft)
@@ -514,8 +502,6 @@ end
function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
if towingVehicle == nil or towedVehicle == nil then return end
TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
if not towingModData or not towedModData then return end
@@ -523,17 +509,25 @@ function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
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
@@ -762,7 +756,6 @@ function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle)
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 index = 0
if script then
index = getTowbarModelSlot(script)
@@ -770,7 +763,7 @@ function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle)
local selectedPart = "towbar"
print("[TowBar DEV] Vehicle: " .. tostring(vehicle:getScriptName()))
print("[TowBar DEV] chassisShape.z = " .. tostring(chassisZ) .. ", half = " .. tostring(halfZ))
print("[TowBar DEV] modelScale = " .. tostring(modelScale) .. ", part = " .. selectedPart)
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 both parts")
for j = 0, TowbarVariantSize - 1 do
+7 -1
View File
@@ -215,7 +215,13 @@ end
function ISVehicleMenu.showRadialMenu(playerObj)
TowBarMod.UI.defaultShowRadialMenu(playerObj)
if playerObj:getVehicle() then return end
local currentVehicle = playerObj:getVehicle()
if currentVehicle then
if TowBarMod.WreckerUI then
TowBarMod.WreckerUI.addOptionsToMenu(playerObj, currentVehicle)
end
return
end
local vehicle = ISVehicleMenu.getVehicleToInteractWith(playerObj)
if vehicle == nil then return end
@@ -0,0 +1,216 @@
if isServer() then return end
if not TowBarMod then TowBarMod = {} end
TowBarMod.WreckerSync = TowBarMod.WreckerSync or {}
local Sync = TowBarMod.WreckerSync
Sync.appliedLevels = Sync.appliedLevels or {}
local function pairKeyContainsVehicle(key, vehicleId)
local id = tostring(vehicleId)
return string.sub(key, 1, #id + 1) == id .. ":"
or string.sub(key, -(#id + 1)) == ":" .. id
end
local function clearAppliedLevelForVehicle(vehicle)
if not vehicle then return end
local vehicleId = vehicle:getId()
for key in pairs(Sync.appliedLevels) do
if pairKeyContainsVehicle(key, vehicleId) then
Sync.appliedLevels[key] = nil
end
end
end
local function pairKey(wrecker, target)
return tostring(wrecker:getId()) .. ":" .. tostring(target:getId())
end
local function callVehicle(vehicle, methodName, value)
local method = vehicle and vehicle[methodName] or nil
if not method then return false, nil end
return pcall(function()
if value ~= nil then return method(vehicle, value) end
return method(vehicle)
end)
end
local function applyFreeRollingState(vehicle)
local md = vehicle and vehicle:getModData() or nil
if not md then return end
local capturedOriginal = false
if md.wreckerOriginalMass == nil then
md.wreckerOriginalMass = vehicle:getMass()
capturedOriginal = true
end
if md.wreckerOriginalBrakingForce == nil then
md.wreckerOriginalBrakingForce = vehicle:getBrakingForce()
capturedOriginal = true
end
if md.wreckerOriginalParkingBrakeOn == nil then
local ok, value = callVehicle(vehicle, "isParkingBrakeOn")
if ok then
md.wreckerOriginalParkingBrakeOn = value
capturedOriginal = true
end
end
if md.wreckerOriginalParkingBrake == nil then
local ok, value = callVehicle(vehicle, "getParkingBrake")
if ok then
md.wreckerOriginalParkingBrake = value
capturedOriginal = true
end
end
if md.wreckerOriginalHandbrake == nil then
local ok, value = callVehicle(vehicle, "isHandbrake")
if ok then
md.wreckerOriginalHandbrake = value
capturedOriginal = true
end
end
vehicle:setMass(200)
vehicle:setBrakingForce(0)
if md.wreckerOriginalParkingBrakeOn ~= nil then callVehicle(vehicle, "setParkingBrakeOn", false) end
if md.wreckerOriginalParkingBrake ~= nil then callVehicle(vehicle, "setParkingBrake", false) end
if md.wreckerOriginalHandbrake ~= nil then callVehicle(vehicle, "setHandbrake", false) end
if capturedOriginal then vehicle:transmitModData() end
end
local function restoreFreeRollingState(vehicle)
local md = vehicle and vehicle:getModData() or nil
if not md then return end
if md.wreckerOriginalMass ~= nil then vehicle:setMass(md.wreckerOriginalMass) end
if md.wreckerOriginalBrakingForce ~= nil then vehicle:setBrakingForce(md.wreckerOriginalBrakingForce) end
if md.wreckerOriginalParkingBrakeOn ~= nil then
callVehicle(vehicle, "setParkingBrakeOn", md.wreckerOriginalParkingBrakeOn)
end
if md.wreckerOriginalParkingBrake ~= nil then
callVehicle(vehicle, "setParkingBrake", md.wreckerOriginalParkingBrake)
end
if md.wreckerOriginalHandbrake ~= nil then
callVehicle(vehicle, "setHandbrake", md.wreckerOriginalHandbrake)
end
md.wreckerOriginalMass = nil
md.wreckerOriginalBrakingForce = nil
md.wreckerOriginalParkingBrakeOn = nil
md.wreckerOriginalParkingBrake = nil
md.wreckerOriginalHandbrake = nil
vehicle:transmitModData()
end
local function isPairLinked(wrecker, target)
return wrecker and target
and wrecker:getVehicleTowing() == target
and target:getVehicleTowedBy() == wrecker
end
local function hasConflictingLink(vehicle, other)
return vehicle and ((vehicle:getVehicleTowing() and vehicle:getVehicleTowing() ~= other)
or (vehicle:getVehicleTowedBy() and vehicle:getVehicleTowedBy() ~= other))
end
local function breakWreckerPair(wrecker, target)
if not wrecker or not target then return end
if isPairLinked(wrecker, target) then
wrecker:breakConstraint(true, true)
end
end
local function setScriptSafely(vehicle, scriptName)
if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
return TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicle, scriptName)
end
vehicle:setScriptName(scriptName)
return true
end
local function applyAttachSync(args)
if not args then return end
local wrecker = getVehicleById(args.wrecker)
local target = getVehicleById(args.target)
if not wrecker or not target then return end
if hasConflictingLink(wrecker, target) or hasConflictingLink(target, wrecker) then return end
local hookAttachment = TowBarMod.Wrecker.getHeightAttachmentId(args.heightLevel)
local targetAttachment = args.targetAttachment
if not hookAttachment or (targetAttachment ~= "trailer" and targetAttachment ~= "trailerfront") then return end
if not wrecker:attachmentExist(hookAttachment) or not target:attachmentExist(targetAttachment) then return end
local key = pairKey(wrecker, target)
local canonicalLevel = tonumber(args.heightLevel) or 0
local appliedLevel = Sync.appliedLevels[key]
if isPairLinked(wrecker, target) and appliedLevel == canonicalLevel then
Sync.appliedLevels[key] = canonicalLevel
applyFreeRollingState(target)
return
end
breakWreckerPair(wrecker, target)
local targetMd = target:getModData()
local originalScript = targetMd.wreckerOriginalScriptName or target:getScriptName()
targetMd.wreckerOriginalScriptName = originalScript
applyFreeRollingState(target)
setScriptSafely(target, "notTowingA_Trailer")
wrecker:addPointConstraint(nil, target, hookAttachment, targetAttachment, true)
setScriptSafely(target, originalScript)
local wreckerMd = wrecker:getModData()
wreckerMd.wreckerTowActive = true
wreckerMd.wreckerTowedVehicleId = target:getId()
wreckerMd.wreckerTowedVehicleSqlId = tonumber(args.targetSqlId)
wreckerMd.wreckerTargetAttachment = targetAttachment
wreckerMd.wreckerHeightLevel = canonicalLevel
targetMd.wreckerTowingVehicleId = wrecker:getId()
targetMd.wreckerTowingVehicleSqlId = tonumber(args.wreckerSqlId)
Sync.appliedLevels[key] = canonicalLevel
end
local function applyDetachSync(args)
if not args then return end
local wrecker = getVehicleById(args.wrecker)
local target = getVehicleById(args.target)
if not target then return end
local targetMd = target:getModData()
local expectedWreckerId = tonumber(targetMd.wreckerTowingVehicleId)
local expectedWreckerSqlId = tonumber(targetMd.wreckerTowingVehicleSqlId)
local commandWreckerSqlId = tonumber(args.wreckerSqlId)
if expectedWreckerSqlId and commandWreckerSqlId then
if expectedWreckerSqlId ~= commandWreckerSqlId then return end
elseif expectedWreckerId and expectedWreckerId ~= tonumber(args.wrecker) then
return
end
if wrecker and (hasConflictingLink(wrecker, target) or hasConflictingLink(target, wrecker)) then return end
if wrecker then breakWreckerPair(wrecker, target) end
if targetMd.wreckerOriginalScriptName and target:getScriptName() ~= targetMd.wreckerOriginalScriptName then
setScriptSafely(target, targetMd.wreckerOriginalScriptName)
end
restoreFreeRollingState(target)
if wrecker then Sync.appliedLevels[pairKey(wrecker, target)] = nil end
if wrecker then
local wreckerMd = wrecker:getModData()
wreckerMd.wreckerTowActive = nil
wreckerMd.wreckerTowedVehicleId = nil
wreckerMd.wreckerTowedVehicleSqlId = nil
wreckerMd.wreckerTargetAttachment = nil
wreckerMd.wreckerHeightLevel = nil
end
targetMd.wreckerTowingVehicleId = nil
targetMd.wreckerTowingVehicleSqlId = nil
targetMd.wreckerOriginalScriptName = nil
end
Sync.applyAttachSync = applyAttachSync
Sync.applyDetachSync = applyDetachSync
Events.OnServerCommand.Add(function(module, command, args)
if module ~= "towbar" then return end
if command == "wreckerAttachSync" then
applyAttachSync(args)
elseif command == "wreckerDetachSync" then
applyDetachSync(args)
end
end)
if Events.OnSpawnVehicleEnd then
Events.OnSpawnVehicleEnd.Add(clearAppliedLevelForVehicle)
end
@@ -0,0 +1,35 @@
require("TimedActions/ISBaseTimedAction")
WreckerTimedAction = ISBaseTimedAction:derive("WreckerTimedAction")
function WreckerTimedAction:isValid()
return self.isValidFunc ~= nil
and self.isValidFunc(self.character, self.arg1, self.arg2) == true
end
function WreckerTimedAction:start()
end
function WreckerTimedAction:perform()
if self:isValid() and self.performFunc then
self.performFunc(self.character, self.arg1, self.arg2)
end
ISBaseTimedAction.perform(self)
end
function WreckerTimedAction:stop()
ISBaseTimedAction.stop(self)
end
function WreckerTimedAction:new(character, time, isValidFunc, performFunc, arg1, arg2)
local action = ISBaseTimedAction.new(self, character)
action.maxTime = time
action.stopOnWalk = false
action.stopOnRun = false
action.useProgressBar = true
action.isValidFunc = isValidFunc
action.performFunc = performFunc
action.arg1 = arg1
action.arg2 = arg2
return action
end
+109
View File
@@ -0,0 +1,109 @@
if isServer() then return end
require("TowBar/WreckerTimedAction")
if not TowBarMod then TowBarMod = {} end
TowBarMod.WreckerUI = TowBarMod.WreckerUI or {}
local UI = TowBarMod.WreckerUI
local Wrecker = TowBarMod.Wrecker
local AttachDuration = 300
local DetachDuration = 200
local function sendAttachCommand(playerObj, wrecker, target)
if not playerObj or not wrecker or not target then return end
sendClientCommand(playerObj, "towbar", "attachWrecker", {
wrecker = wrecker:getId(),
target = target:getId()
})
end
local function sendDetachCommand(playerObj, wrecker, target)
if not playerObj or not wrecker or not target then return end
sendClientCommand(playerObj, "towbar", "detachWrecker", {
wrecker = wrecker:getId(),
target = target:getId()
})
end
local function isAttachStillValid(playerObj, wrecker, target)
if not playerObj or not wrecker or not target then return false end
if not wrecker:isDriver(playerObj) or not Wrecker.isSupportedWrecker(wrecker) then return false end
if wrecker:getVehicleTowing() or wrecker:getVehicleTowedBy()
or target:getVehicleTowing() or target:getVehicleTowedBy() then return false end
local nearest = Wrecker.resolveNearestTarget(wrecker, Wrecker.getWorldVehicles())
return nearest ~= nil and nearest.vehicle == target
end
local function isDetachStillValid(playerObj, wrecker, target)
if not playerObj or not wrecker or not target or not wrecker:isDriver(playerObj) then return false end
local md = wrecker:getModData()
return wrecker:getVehicleTowing() == target
or (md and tonumber(md.wreckerTowedVehicleId) == target:getId())
end
local function requestAttach(playerObj, wrecker, target)
if not isAttachStillValid(playerObj, wrecker, target) then return end
ISTimedActionQueue.add(WreckerTimedAction:new(
playerObj, AttachDuration, isAttachStillValid, sendAttachCommand, wrecker, target
))
end
local function requestDetach(playerObj, wrecker, target)
if not isDetachStillValid(playerObj, wrecker, target) then return end
ISTimedActionQueue.add(WreckerTimedAction:new(
playerObj, DetachDuration, isDetachStillValid, sendDetachCommand, wrecker, target
))
end
local function requestHeight(playerObj, wrecker, direction)
if not playerObj or not wrecker then return end
sendClientCommand(playerObj, "towbar", "adjustWreckerHeight", {
wrecker = wrecker:getId(),
direction = direction
})
end
function UI.addOptionsToMenu(playerObj, wrecker)
if not playerObj or not wrecker or not Wrecker.isSupportedWrecker(wrecker) then return false end
if not wrecker:isDriver(playerObj) then return false end
local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
if not menu then return false end
local md = wrecker:getModData()
local targetId = md and tonumber(md.wreckerTowedVehicleId) or nil
local target = targetId and getVehicleById(targetId) or wrecker:getVehicleTowing()
if target and (target == wrecker:getVehicleTowing() or targetId == target:getId()) then
TowBarMod.UI.removeDefaultDetachOption(playerObj)
local level = tonumber(md.wreckerHeightLevel) or 0
if level < Wrecker.MaxHeightLevel then
menu:addSlice(
getText("UI_Text_Towing_heightUp"),
getTexture("media/textures/arrow_up.png"),
requestHeight, playerObj, wrecker, 1
)
end
if level > Wrecker.MinHeightLevel then
menu:addSlice(
getText("UI_Text_Towing_heightDown"),
getTexture("media/textures/arrow_down.png"),
requestHeight, playerObj, wrecker, -1
)
end
menu:addSlice(
getText("UI_Text_Towing_detachHook", ISVehicleMenu.getVehicleDisplayName(target)),
getTexture("media/textures/untow_car_icon.png"),
requestDetach, playerObj, wrecker, target
)
return true
end
if wrecker:getVehicleTowing() or wrecker:getVehicleTowedBy() then return false end
local nearest = Wrecker.resolveNearestTarget(wrecker, Wrecker.getWorldVehicles())
if not nearest then return false end
menu:addSlice(
getText("UI_Text_Towing_attachHook", ISVehicleMenu.getVehicleDisplayName(nearest.vehicle)),
getTexture("media/textures/tow_car_icon.png"),
requestAttach, playerObj, wrecker, nearest.vehicle
)
return true
end