diff --git a/42.20/media/lua/client/TowBar/Config.lua b/42.20/media/lua/client/TowBar/Config.lua
new file mode 100644
index 0000000..abcff59
--- /dev/null
+++ b/42.20/media/lua/client/TowBar/Config.lua
@@ -0,0 +1,10 @@
+if not TowBarMod then TowBarMod = {} end
+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.20/media/lua/client/TowBar/CustomPathFindAction.lua b/42.20/media/lua/client/TowBar/CustomPathFindAction.lua
new file mode 100644
index 0000000..75cfbcd
--- /dev/null
+++ b/42.20/media/lua/client/TowBar/CustomPathFindAction.lua
@@ -0,0 +1,72 @@
+require "TimedActions/ISBaseTimedAction"
+
+TowBarCustomPathFind = ISBaseTimedAction:derive("TowBarCustomPathFind")
+
+function TowBarCustomPathFind:isValid()
+ return true
+end
+
+function TowBarCustomPathFind:update()
+ if instanceof(self.character, "IsoPlayer") and
+ (self.character:pressedMovement(false) or self.character:pressedCancelAction()) then
+ self:forceStop()
+ return
+ end
+
+ local result = self.character:getPathFindBehavior2():update()
+ if result == BehaviorResult.Succeeded then
+ self:forceComplete()
+ end
+
+ local x = self.character:getX()
+ local y = self.character:getY()
+
+ if x == self.lastX and y == self.lastY then
+ self.currentTimeInOnePosition = self.currentTimeInOnePosition + 1
+ else
+ self.currentTimeInOnePosition = 0
+ self.lastX = x
+ self.lastY = y
+ end
+
+ if self.currentTimeInOnePosition > self.maxTimeInOnePosition then
+ self:forceComplete()
+ end
+end
+
+function TowBarCustomPathFind:start()
+ self.character:facePosition(self.goal[2], self.goal[3])
+ self.character:getPathFindBehavior2():pathToLocationF(self.goal[2], self.goal[3], self.goal[4])
+end
+
+function TowBarCustomPathFind:stop()
+ ISBaseTimedAction.stop(self)
+ self.character:getPathFindBehavior2():cancel()
+ self.character:setPath2(nil)
+end
+
+function TowBarCustomPathFind:perform()
+ self.character:getPathFindBehavior2():cancel()
+ self.character:setPath2(nil)
+ ISBaseTimedAction.perform(self)
+end
+
+function TowBarCustomPathFind:pathToLocationF(character, targetX, targetY, targetZ)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.character = character
+ o.stopOnWalk = false
+ o.stopOnRun = false
+ o.maxTime = -1
+
+ o.maxTimeInOnePosition = 15
+ o.currentTimeInOnePosition = 0
+ o.lastX = -1
+ o.lastY = -1
+
+ o.goal = { 'LocationF', targetX, targetY, targetZ }
+ return o
+end
+
+
diff --git a/42.20/media/lua/client/TowBar/HookVehicleAction.lua b/42.20/media/lua/client/TowBar/HookVehicleAction.lua
new file mode 100644
index 0000000..4144d8f
--- /dev/null
+++ b/42.20/media/lua/client/TowBar/HookVehicleAction.lua
@@ -0,0 +1,56 @@
+require('TimedActions/ISBaseTimedAction')
+
+TowBarHookVehicle = ISBaseTimedAction:derive("TowBarHookVehicle")
+
+
+-- The condition which tells the timed action if it is still valid
+function TowBarHookVehicle:isValid()
+ return true;
+end
+
+-- Starts the Timed Action
+function TowBarHookVehicle:start()
+ self:setActionAnim(self.animation)
+ self.sound = getSoundManager():PlayWorldSound("towbar_hookingSound", false, self.character:getSquare(), 0, 5, 1, true)
+end
+
+-- Is called when the time has passed
+function TowBarHookVehicle:perform()
+ self.sound:stop();
+
+ if self.performFunc ~= nil then
+ self.performFunc(self.character, self.arg1, self.arg2, self.arg3, self.arg4)
+ end
+
+ ISBaseTimedAction.perform(self);
+end
+
+
+function TowBarHookVehicle:stop()
+ if self.sound then
+ self.sound:stop()
+ end
+
+ ISBaseTimedAction.stop(self)
+end
+
+function TowBarHookVehicle:new(character, time, animation, performFunc, arg1, arg2, arg3, arg4)
+ local o = {};
+ setmetatable(o, self)
+ self.__index = self
+ o.stopOnWalk = true
+ o.stopOnRun = true
+ o.maxTime = time
+
+ o.character = character;
+ o.animation = animation
+
+ o.performFunc = performFunc
+ o.arg1 = arg1
+ o.arg2 = arg2
+ o.arg3 = arg3
+ o.arg4 = arg4
+
+ return o;
+end
+
diff --git a/42.20/media/lua/client/TowBar/ScheduleAction.lua b/42.20/media/lua/client/TowBar/ScheduleAction.lua
new file mode 100644
index 0000000..9baec82
--- /dev/null
+++ b/42.20/media/lua/client/TowBar/ScheduleAction.lua
@@ -0,0 +1,42 @@
+require("TimedActions/ISBaseTimedAction")
+
+TowBarScheduleAction = ISBaseTimedAction:derive("TowBarScheduleAction")
+
+function TowBarScheduleAction:isValid()
+ return true
+end
+
+function TowBarScheduleAction:start()
+end
+
+function TowBarScheduleAction:perform()
+ if self.performFunc ~= nil then
+ self.performFunc(self.character, self.arg1, self.arg2, self.arg3, self.arg4)
+ end
+
+ ISBaseTimedAction.perform(self)
+end
+
+function TowBarScheduleAction:stop()
+ ISBaseTimedAction.stop(self)
+end
+
+function TowBarScheduleAction:new(character, time, performFunc, arg1, arg2, arg3, arg4)
+ local o = ISBaseTimedAction.new(self, character)
+
+ o.useProgressBar = false
+ o.stopOnWalk = false
+ o.stopOnRun = false
+
+ o.maxTime = time
+ o.character = character
+
+ o.performFunc = performFunc
+ o.arg1 = arg1
+ o.arg2 = arg2
+ o.arg3 = arg3
+ o.arg4 = arg4
+
+ return o
+end
+
diff --git a/42.20/media/lua/client/TowBar/TowSyncClient.lua b/42.20/media/lua/client/TowBar/TowSyncClient.lua
new file mode 100644
index 0000000..2c58639
--- /dev/null
+++ b/42.20/media/lua/client/TowBar/TowSyncClient.lua
@@ -0,0 +1,173 @@
+if isServer() then return end
+
+if not TowBarMod then TowBarMod = {} end
+TowBarMod.Sync = TowBarMod.Sync or {}
+if TowBarMod.Sync._towSyncClientLoaded then return end
+TowBarMod.Sync._towSyncClientLoaded = true
+
+local function resolveVehicle(id)
+ if not id then return nil end
+ return getVehicleById(id)
+end
+
+local function ensureAttachment(vehicle, attachmentId)
+ if not vehicle or not attachmentId then return false end
+
+ local script = vehicle:getScript()
+ if not script then return false end
+ if script:getAttachmentById(attachmentId) ~= nil then return true end
+
+ local wheelCount = script:getWheelCount()
+ local yOffset = -0.5
+ if wheelCount > 0 then
+ local wheel = script:getWheel(0)
+ if wheel and wheel:getOffset() then
+ yOffset = wheel:getOffset():y() + 0.1
+ end
+ end
+
+ local chassis = script:getPhysicsChassisShape()
+ if not chassis then return false end
+
+ local attach = ModelAttachment.new(attachmentId)
+ if attachmentId == "trailer" then
+ attach:getOffset():set(0, yOffset, -chassis:z() / 2 - 0.1)
+ attach:setZOffset(-1)
+ else
+ attach:getOffset():set(0, yOffset, chassis:z() / 2 + 0.1)
+ attach:setZOffset(1)
+ end
+ script:addAttachment(attach)
+ return true
+end
+
+local function isLinked(vehicleA, vehicleB)
+ if not vehicleA or not vehicleB then return false end
+ return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA
+end
+
+local function reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
+ if TowBarMod.Utils and TowBarMod.Utils.updateAttachmentsForRigidTow then
+ TowBarMod.Utils.updateAttachmentsForRigidTow(vehicleA, vehicleB, attachmentA, attachmentB)
+ end
+
+ local towingMd = vehicleA:getModData()
+ local towedMd = vehicleB:getModData()
+ local currentScript = vehicleB:getScriptName()
+
+ if towingMd then
+ towingMd["isTowingByTowBar"] = true
+ towingMd["towed"] = false
+ towingMd["towBarTowedVehicleId"] = vehicleB:getId()
+ towingMd["towBarTowingVehicleId"] = nil
+ towingMd["towBarExpectedAttachment"] = attachmentA
+ vehicleA:transmitModData()
+ end
+ if towedMd then
+ if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then
+ towedMd.towBarOriginalScriptName = currentScript
+ end
+ if towedMd.towBarOriginalMass == nil then
+ towedMd.towBarOriginalMass = vehicleB:getMass()
+ end
+ if towedMd.towBarOriginalBrakingForce == nil then
+ towedMd.towBarOriginalBrakingForce = vehicleB:getBrakingForce()
+ end
+ towedMd["isTowingByTowBar"] = true
+ towedMd["towed"] = true
+ towedMd["towBarTowedVehicleId"] = nil
+ towedMd["towBarTowingVehicleId"] = vehicleA:getId()
+ towedMd["towBarExpectedAttachment"] = attachmentB
+ vehicleB:transmitModData()
+ end
+
+ if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
+ TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicleB, "notTowingA_Trailer")
+ end
+ if TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
+ pcall(TowBarMod.Hook.setVehiclePostAttach, nil, vehicleB)
+ end
+end
+
+local function applyAttachSync(args)
+ if not args then return end
+
+ local vehicleA = resolveVehicle(args.vehicleA)
+ local vehicleB = resolveVehicle(args.vehicleB)
+ if not vehicleA or not vehicleB then return end
+
+ local attachmentA = args.attachmentA or "trailer"
+ local attachmentB = args.attachmentB or "trailerfront"
+ if not ensureAttachment(vehicleA, attachmentA) or not ensureAttachment(vehicleB, attachmentB) then
+ return
+ end
+
+ if not isLinked(vehicleA, vehicleB) then
+ vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB)
+ end
+
+ reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
+end
+
+local function hasConflictingTowLink(vehicle, expectedOther)
+ if not vehicle or not expectedOther then return false end
+
+ local towing = vehicle:getVehicleTowing()
+ local towedBy = vehicle:getVehicleTowedBy()
+ if (towing ~= nil and towing ~= expectedOther)
+ or (towedBy ~= nil and towedBy ~= expectedOther) then
+ return true
+ end
+
+ -- Also reject a delayed detach while a new tow is being established but
+ -- has not created its physical constraint yet.
+ local modData = vehicle:getModData()
+ if not modData then return false end
+ local expectedOtherId = expectedOther:getId()
+ return (modData["towBarTowedVehicleId"] ~= nil
+ and modData["towBarTowedVehicleId"] ~= expectedOtherId)
+ or (modData["towBarTowingVehicleId"] ~= nil
+ and modData["towBarTowingVehicleId"] ~= expectedOtherId)
+end
+
+local function breakTowBarPair(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)
+ end
+end
+
+local function applyDetachSync(args)
+ if not args then return end
+ local vehicleA = resolveVehicle(args.vehicleA)
+ local vehicleB = resolveVehicle(args.vehicleB)
+ if not vehicleA or not vehicleB then return end
+ if hasConflictingTowLink(vehicleA, vehicleB) or hasConflictingTowLink(vehicleB, vehicleA) then
+ return
+ end
+ breakTowBarPair(vehicleA, vehicleB)
+
+ if TowBarMod.Hook and TowBarMod.Hook.cleanupDetachedTowBar then
+ pcall(TowBarMod.Hook.cleanupDetachedTowBar, vehicleA, vehicleB)
+ end
+end
+
+local function onServerCommand(module, command, args)
+ if module ~= "towbar" then return end
+
+ if command == "forceAttachSync" then
+ applyAttachSync(args)
+ elseif command == "forceDetachSync" or command == "spontaneousDetachSync" then
+ applyDetachSync(args)
+ end
+end
+
+Events.OnServerCommand.Add(onServerCommand)
diff --git a/42.20/media/lua/client/TowBar/TowingHooking.lua b/42.20/media/lua/client/TowBar/TowingHooking.lua
new file mode 100644
index 0000000..6947150
--- /dev/null
+++ b/42.20/media/lua/client/TowBar/TowingHooking.lua
@@ -0,0 +1,833 @@
+if not TowBarMod then TowBarMod = {} end
+if not TowBarMod.Hook then TowBarMod.Hook = {} end
+
+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)
+
+ 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
+
+ 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
+
+ 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 sendTowAttachCommand(playerObj, args)
+ if not playerObj or not args then return end
+
+ -- MP-safe/server-authoritative attach path (Landtrain style).
+ if isClient() and isMultiplayer() then
+ sendClientCommand(playerObj, "towbar", "attachTowBar", args)
+ return
+ end
+
+ -- Keep vanilla attach path for SP/local behavior.
+ sendClientCommand(playerObj, "vehicle", "attachTrailer", args)
+end
+
+local TowbarVariantSize = 24
+local TowbarNormalStart = 0
+local TowbarLargeStart = 24
+local TowbarMaxIndex = TowbarVariantSize - 1
+local VanillaScaleMin = 1.5
+local VanillaScaleMax = 2.0
+
+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 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))
+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)
+end
+
+local function setTowBarModelVisible(vehicle, isVisible)
+ if not vehicle then return end
+
+ local normalPart = vehicle:getPartById("towbar")
+ local largePart = vehicle:getPartById("towbarLarge")
+ if normalPart == nil and largePart == 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
+ end
+
+ if not isVisible then
+ vehicle:doDamageOverlay()
+ return
+ end
+
+ local script = vehicle:getScript()
+ if not script then
+ vehicle:doDamageOverlay()
+ return
+ end
+
+ local index = getTowbarModelSlot(script)
+ local part = normalPart
+ if part 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 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()
+ applyFreeRollingTowState(towedVehicle)
+
+ towingModData["isTowingByTowBar"] = true
+ towingModData["towed"] = false
+ towingModData["towBarTowedVehicleId"] = towedVehicle:getId()
+ towingModData["towBarTowingVehicleId"] = nil
+ towingModData["towBarExpectedAttachment"] = attachmentA
+ towedModData["isTowingByTowBar"] = true
+ towedModData["towed"] = true
+ towedModData["towBarTowedVehicleId"] = nil
+ towedModData["towBarTowingVehicleId"] = towingVehicle:getId()
+ towedModData["towBarExpectedAttachment"] = attachmentB
+ towingVehicle:transmitModData()
+ towedVehicle:transmitModData()
+
+ TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, "notTowingA_Trailer")
+
+ local args = {
+ vehicleA = towingVehicle:getId(),
+ vehicleB = towedVehicle:getId(),
+ attachmentA = attachmentA,
+ attachmentB = attachmentB
+ }
+ sendTowAttachCommand(playerObj, args)
+ ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
+ return true
+end
+
+local function reattachTowBarPairAfterCleanDetach(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 detachArgs = {
+ towingVehicle = towingVehicle:getId(),
+ vehicle = towedVehicle:getId()
+ }
+ sendClientCommand(playerObj, "towbar", "detachTowBar", detachArgs)
+
+ -- World load/spawn can restore constraints in a bad state. Reattach one
+ -- short tick later so the detach is fully applied first.
+ ISTimedActionQueue.add(TowBarScheduleAction:new(
+ playerObj,
+ 1,
+ reattachTowBarPair,
+ towingVehicle,
+ towedVehicle,
+ requireDriver
+ ))
+ 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 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.
+ TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
+ end
+
+ if localPlayer and towingVehicle then
+ if reattachTowBarPairAfterCleanDetach(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
+ TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, towedModData.towBarOriginalScriptName)
+ end
+
+ local towingVehicle = towedVehicle:getVehicleTowedBy()
+ 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
+ TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
+ 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 and not (isClient() and isMultiplayer()) 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()
+ applyFreeRollingTowState(towedVehicle)
+
+ towingModData["isTowingByTowBar"] = true
+ towingModData["towed"] = false
+ towingModData["towBarTowedVehicleId"] = towedVehicle:getId()
+ towingModData["towBarTowingVehicleId"] = nil
+ towingModData["towBarExpectedAttachment"] = attachmentA
+ towedModData["isTowingByTowBar"] = true
+ towedModData["towed"] = true
+ towedModData["towBarTowedVehicleId"] = nil
+ towedModData["towBarTowingVehicleId"] = towingVehicle:getId()
+ towedModData["towBarExpectedAttachment"] = attachmentB
+ towingVehicle:transmitModData()
+ towedVehicle:transmitModData()
+
+ -- Match the known-good rigid tow path: fake trailer + vanilla attach command.
+ TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, "notTowingA_Trailer")
+
+ local args = {
+ vehicleA = towingVehicle:getId(),
+ vehicleB = towedVehicle:getId(),
+ attachmentA = attachmentA,
+ attachmentB = attachmentB,
+ itemId = towBarItem and towBarItem:getID() or nil
+ }
+ sendTowAttachCommand(playerObj, args)
+ ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
+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
+
+ if towedModData.towBarOriginalScriptName then
+ TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, towedModData.towBarOriginalScriptName)
+ end
+ restoreFreeRollingTowState(towedVehicle, towedModData)
+
+ towingModData["isTowingByTowBar"] = false
+ towingModData["towed"] = false
+ towingModData["towBarTowedVehicleId"] = nil
+ towingModData["towBarTowingVehicleId"] = nil
+ towingModData["towBarExpectedAttachment"] = nil
+ towedModData["isTowingByTowBar"] = false
+ towedModData["towed"] = false
+ towedModData["towBarTowedVehicleId"] = nil
+ towedModData["towBarTowingVehicleId"] = 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()
+
+ TowBarMod.Hook.lastAutoReattachAtByVehicle[towingVehicle:getId()] = nil
+ 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)
+
+ TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
+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
+
+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.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()
+ forEachCollectionItem(vehicles, function(vehicle)
+ recoverTowBarVehicleAfterLoad(playerObj, vehicle, 6)
+ end)
+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")
+ if normalPart == nil and largePart == nil then
+ print("[TowBar DEV] No 'towbar' or 'towbarLarge' 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 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] modelScale = " .. tostring(modelScale) .. ", 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
+ if normalPart then normalPart:setModelVisible("towbar" .. j, true) end
+ if largePart then largePart: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")
+ if normalPart == nil and largePart == nil then
+ print("[TowBar DEV] No 'towbar' or 'towbarLarge' 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
+ 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")
+ if normalPart == nil and largePart == nil then
+ print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
+ return
+ end
+
+ local localIndex = math.max(0, math.min(TowbarMaxIndex, index % TowbarVariantSize))
+ local useLargePart = index >= TowbarVariantSize
+
+ for j = 0, TowbarVariantSize - 1 do
+ if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
+ if largePart then largePart:setModelVisible("towbar" .. j, false) end
+ end
+
+ local part = useLargePart and largePart or normalPart
+ if part == nil then
+ part = normalPart or largePart
+ end
+ print("[TowBar DEV] Showing only towbar" .. tostring(localIndex) .. " on part " .. tostring(useLargePart and "towbarLarge" or "towbar") .. " (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.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle)
+Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat)
+Events.OnTick.Add(keepTowBarVehiclesFreeRolling)
diff --git a/42.20/media/lua/client/TowBar/TowingUI.lua b/42.20/media/lua/client/TowBar/TowingUI.lua
new file mode 100644
index 0000000..e80a87a
--- /dev/null
+++ b/42.20/media/lua/client/TowBar/TowingUI.lua
@@ -0,0 +1,231 @@
+if not TowBarMod then TowBarMod = {} end
+if not TowBarMod.UI then TowBarMod.UI = {} end
+
+---------------------------------------------------------------------------
+--- UI functions
+---------------------------------------------------------------------------
+
+function TowBarMod.UI.removeDefaultDetachOption(playerObj)
+ local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
+ if menu == nil then return end
+
+ local tmpSlices = {}
+ for i, slice in ipairs(menu.slices or {}) do
+ tmpSlices[i] = slice
+ end
+ menu:clear()
+ for _, slice in ipairs(tmpSlices) do
+ local command = slice.command and slice.command[1]
+ local args = slice.command or {}
+ if command ~= ISVehicleMenu.onDetachTrailer then
+ menu:addSlice(
+ slice.text,
+ slice.texture,
+ args[1],
+ args[2],
+ args[3],
+ args[4],
+ args[5],
+ args[6],
+ args[7]
+ )
+ end
+ end
+end
+
+--- Show menu with available vehicles for tow bar hook.
+function TowBarMod.UI.showChooseVehicleMenu(playerObj, vehicle, vehicles, hasTowBar)
+ local playerIndex = playerObj:getPlayerNum()
+ local menu = getPlayerRadialMenu(playerIndex)
+ menu:clear()
+
+ local added = 0
+ for _, veh in ipairs(vehicles) do
+ local hookTypeVariants = TowBarMod.Utils.getHookTypeVariants(vehicle, veh, hasTowBar)
+ if #hookTypeVariants > 0 then
+ local hookType = hookTypeVariants[1]
+ menu:addSlice(
+ hookType.name,
+ getTexture("media/textures/tow_bar_attach.png"),
+ hookType.func,
+ playerObj,
+ hookType.towingVehicle,
+ hookType.towedVehicle,
+ hookType.towingPoint,
+ hookType.towedPoint
+ )
+ added = added + 1
+ end
+ end
+
+ if added == 0 then return end
+
+ menu:setX(getPlayerScreenLeft(playerIndex) + getPlayerScreenWidth(playerIndex) / 2 - menu:getWidth() / 2)
+ menu:setY(getPlayerScreenTop(playerIndex) + getPlayerScreenHeight(playerIndex) / 2 - menu:getHeight() / 2)
+ menu:addToUIManager()
+ if JoypadState.players[playerObj:getPlayerNum()+1] then
+ menu:setHideWhenButtonReleased(Joypad.DPadUp)
+ setJoypadFocus(playerObj:getPlayerNum(), menu)
+ playerObj:setJoypadIgnoreAimUntilCentered(true)
+ end
+end
+
+function TowBarMod.UI.addHookOptionToMenu(playerObj, vehicle)
+ local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
+ if menu == nil then return end
+
+ local hasTowBar = playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar") ~= nil
+ if not hasTowBar then return end
+
+ local vehicles = TowBarMod.Utils.getAviableVehicles(vehicle, hasTowBar)
+
+ if #vehicles == 0 then
+ return
+ elseif #vehicles == 1 then
+ local hookTypeVariants = TowBarMod.Utils.getHookTypeVariants(vehicle, vehicles[1], hasTowBar)
+ if #hookTypeVariants > 0 then
+ local hookType = hookTypeVariants[1]
+ menu:addSlice(
+ hookType.name,
+ getTexture("media/textures/tow_bar_attach.png"),
+ hookType.func,
+ playerObj,
+ hookType.towingVehicle,
+ hookType.towedVehicle,
+ hookType.towingPoint,
+ hookType.towedPoint
+ )
+ end
+ else
+ menu:addSlice(
+ getText("UI_Text_Towing_attach") .. "...",
+ getTexture("media/textures/tow_bar_attach.png"),
+ TowBarMod.UI.showChooseVehicleMenu,
+ playerObj,
+ vehicle,
+ vehicles,
+ hasTowBar
+ )
+ end
+end
+
+function TowBarMod.UI.addUnhookOptionToMenu(playerObj, vehicle)
+ local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
+ if menu == nil then return end
+ if not vehicle:getModData()["isTowingByTowBar"] then return end
+ if not vehicle:getVehicleTowing() and not vehicle:getVehicleTowedBy() then return end
+
+ local towedVehicle = vehicle
+ if vehicle:getVehicleTowing() then
+ towedVehicle = vehicle:getVehicleTowing()
+ end
+
+ menu:addSlice(
+ getText("ContextMenu_Vehicle_DetachTrailer", ISVehicleMenu.getVehicleDisplayName(towedVehicle)),
+ getTexture("media/textures/tow_bar_detach.png"),
+ TowBarMod.Hook.deattachTowBarAction,
+ playerObj,
+ towedVehicle
+ )
+end
+
+
+---------------------------------------------------------------------------
+--- Dev menu
+---------------------------------------------------------------------------
+
+function TowBarMod.UI.showDevSingleTowbarMenu(playerObj, vehicle)
+ local playerIndex = playerObj:getPlayerNum()
+ local menu = getPlayerRadialMenu(playerIndex)
+ menu:clear()
+
+ for j = 0, 47 do
+ local zIndex = j % 24
+ local modelType = (j >= 24) and "large" or "normal"
+ menu:addSlice(
+ "towbar" .. j .. " [" .. modelType .. "] (Z=" .. tostring(1.0 + zIndex * 0.1) .. ")",
+ getTexture("media/textures/tow_bar_icon.png"),
+ TowBarMod.Hook.devShowSingleTowbar,
+ playerObj,
+ vehicle,
+ j
+ )
+ end
+
+ menu:setX(getPlayerScreenLeft(playerIndex) + getPlayerScreenWidth(playerIndex) / 2 - menu:getWidth() / 2)
+ menu:setY(getPlayerScreenTop(playerIndex) + getPlayerScreenHeight(playerIndex) / 2 - menu:getHeight() / 2)
+ menu:addToUIManager()
+ if JoypadState.players[playerObj:getPlayerNum()+1] then
+ menu:setHideWhenButtonReleased(Joypad.DPadUp)
+ setJoypadFocus(playerObj:getPlayerNum(), menu)
+ playerObj:setJoypadIgnoreAimUntilCentered(true)
+ end
+end
+
+function TowBarMod.UI.addDevOptionsToMenu(playerObj, vehicle)
+ local devModeEnabled = (TowBarMod.Config and TowBarMod.Config.devMode) or getDebug()
+ if not devModeEnabled then return end
+ if not vehicle then return end
+
+ local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
+ if menu == nil then return end
+
+ menu:addSlice(
+ "[DEV] Show ALL Towbars",
+ getTexture("media/textures/tow_bar_icon.png"),
+ TowBarMod.Hook.devShowAllTowbarModels,
+ playerObj,
+ vehicle
+ )
+
+ menu:addSlice(
+ "[DEV] Hide ALL Towbars",
+ getTexture("media/textures/tow_bar_icon.png"),
+ TowBarMod.Hook.devHideAllTowbarModels,
+ playerObj,
+ vehicle
+ )
+
+ menu:addSlice(
+ "[DEV] Pick Single Towbar...",
+ getTexture("media/textures/tow_bar_icon.png"),
+ TowBarMod.UI.showDevSingleTowbarMenu,
+ playerObj,
+ vehicle
+ )
+end
+
+---------------------------------------------------------------------------
+--- Mod compability
+---------------------------------------------------------------------------
+
+if getActivatedMods():contains("vehicle_additions") then
+ require("Vehicles/ISUI/Oven_Mattress_RadialMenu")
+ require("Vehicles/ISUI/FuelTruckTank_ISVehicleMenu_FillPartMenu")
+end
+
+---------------------------------------------------------------------------
+--- Attach to default menu method
+---------------------------------------------------------------------------
+
+if TowBarMod.UI.defaultShowRadialMenu == nil then
+ TowBarMod.UI.defaultShowRadialMenu = ISVehicleMenu.showRadialMenu
+end
+
+function ISVehicleMenu.showRadialMenu(playerObj)
+ TowBarMod.UI.defaultShowRadialMenu(playerObj)
+
+ if playerObj:getVehicle() then return end
+
+ local vehicle = ISVehicleMenu.getVehicleToInteractWith(playerObj)
+ if vehicle == nil then return end
+
+ if vehicle:getModData()["isTowingByTowBar"] then
+ TowBarMod.UI.removeDefaultDetachOption(playerObj)
+ TowBarMod.UI.addUnhookOptionToMenu(playerObj, vehicle)
+ elseif not vehicle:getVehicleTowing() and not vehicle:getVehicleTowedBy() then
+ TowBarMod.UI.addHookOptionToMenu(playerObj, vehicle)
+ end
+
+ TowBarMod.UI.addDevOptionsToMenu(playerObj, vehicle)
+end
diff --git a/42.20/media/lua/client/TowBar/TowingUtils.lua b/42.20/media/lua/client/TowBar/TowingUtils.lua
new file mode 100644
index 0000000..52ba980
--- /dev/null
+++ b/42.20/media/lua/client/TowBar/TowingUtils.lua
@@ -0,0 +1,317 @@
+if not TowBarMod then TowBarMod = {} end
+if not TowBarMod.Utils then TowBarMod.Utils = {} end
+
+TowBarMod.Utils.tempVector1 = Vector3f.new()
+TowBarMod.Utils.tempVector2 = Vector3f.new()
+
+---------------------------------------------------------------------------
+--- Util functions
+---------------------------------------------------------------------------
+
+--- Compute the attachment Y offset for a vehicle so the towbar sits just
+--- above the wheels (i.e. a fixed distance off the ground) regardless of
+--- how the vehicle model is configured.
+local function computeAttachmentHeight(vehicle)
+ local script = vehicle:getScript()
+ if not script then return -0.5 end
+
+ local wheelCount = script:getWheelCount()
+ if wheelCount > 0 then
+ return script:getWheel(0):getOffset():y() + 0.1
+ end
+
+ 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
+
+--- Return vehicles from sector that player can tow by tow bar.
+function TowBarMod.Utils.getAviableVehicles(mainVehicle, hasTowBar)
+ local vehicles = {}
+ if not hasTowBar then return vehicles end
+
+ local square = mainVehicle:getSquare()
+ if square == nil then return vehicles end
+
+ -- Match vanilla towing search radius.
+ for y=square:getY() - 6, square:getY() + 6 do
+ for x=square:getX() - 6, square:getX() + 6 do
+ local square2 = getCell():getGridSquare(x, y, square:getZ())
+ if square2 then
+ for i=1, square2:getMovingObjects():size() do
+ local obj = square2:getMovingObjects():get(i-1)
+ if obj ~= nil
+ and instanceof(obj, "BaseVehicle")
+ and obj ~= mainVehicle
+ and #(TowBarMod.Utils.getHookTypeVariants(mainVehicle, obj, hasTowBar)) ~= 0 then
+ table.insert(vehicles, obj)
+ end
+ end
+ end
+ end
+ end
+
+ return vehicles
+end
+
+--- Return a table with towbar-only hook options for vehicles.
+function TowBarMod.Utils.getHookTypeVariants(vehicleA, vehicleB, hasTowBar)
+ local hookTypeVariants = {}
+ if not hasTowBar then return hookTypeVariants end
+
+ if vehicleA:getVehicleTowing() or vehicleA:getVehicleTowedBy()
+ or vehicleB:getVehicleTowing() or vehicleB:getVehicleTowedBy() then
+ return hookTypeVariants
+ end
+
+ -- Keep tow bars for vehicle-to-vehicle towing only.
+ if TowBarMod.Utils.isTrailer(vehicleA) or TowBarMod.Utils.isTrailer(vehicleB) then
+ return hookTypeVariants
+ end
+
+ if vehicleA:canAttachTrailer(vehicleB, "trailerfront", "trailer") then
+ local hookType = {}
+ hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getText("UI_Text_Towing_byTowBar")
+ hookType.func = TowBarMod.Hook.attachByTowBarAction
+ hookType.towingVehicle = vehicleB
+ hookType.towedVehicle = vehicleA
+ hookType.textureName = "tow_bar_icon"
+ table.insert(hookTypeVariants, hookType)
+ elseif vehicleA:canAttachTrailer(vehicleB, "trailer", "trailerfront") then
+ local hookType = {}
+ hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getText("UI_Text_Towing_byTowBar")
+ hookType.func = TowBarMod.Hook.attachByTowBarAction
+ hookType.towingVehicle = vehicleA
+ hookType.towedVehicle = vehicleB
+ hookType.textureName = "tow_bar_icon"
+ table.insert(hookTypeVariants, hookType)
+ end
+
+ return hookTypeVariants
+end
+
+function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, 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)
+ towingAttachment:setZOffset(0)
+
+ towedAttachment:setUpdateConstraint(false)
+ towedAttachment:setZOffset(0)
+
+ -- Dynamic height: compute Y from wheel offset so the towbar never clips the floor.
+ local towingHeight = computeAttachmentHeight(towingVehicle)
+ local towedHeight = computeAttachmentHeight(towedVehicle)
+
+ -- Store and update the towing vehicle's attachment Y.
+ local towingModData = towingVehicle:getModData()
+ 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 towedModData = towedVehicle:getModData()
+ local spacingDistance = 1.0
+ if TowBarMod.Config and tonumber(TowBarMod.Config.rigidTowbarDistance) ~= nil then
+ 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"])
+ local storedBaseZ = tonumber(towedModData["towBarBaseAttachmentOffsetZ"])
+ local hasStoredBase = towedModData["towBarBaseAttachmentId"] == attachmentB
+ and storedBaseX ~= nil and storedBaseY ~= nil and storedBaseZ ~= nil
+
+ local baseX = hasStoredBase and storedBaseX or offset:x()
+ local baseY = hasStoredBase and storedBaseY or offset:y()
+ local baseZ = hasStoredBase and storedBaseZ or offset:z()
+
+ if not hasStoredBase then
+ towedModData["towBarBaseAttachmentId"] = attachmentB
+ towedModData["towBarBaseAttachmentOffsetX"] = baseX
+ towedModData["towBarBaseAttachmentOffsetY"] = baseY
+ towedModData["towBarBaseAttachmentOffsetZ"] = baseZ
+ end
+
+ local towedExteriorZ = getExteriorAttachmentZ(towedScript, attachmentB, baseZ, spacingDistance)
+ local zShift = towedExteriorZ - baseZ
+ towedAttachment:getOffset():set(baseX, towedHeight, towedExteriorZ)
+
+ towedModData["isChangedTowedAttachment"] = true
+ towedModData["towBarChangedAttachmentId"] = attachmentB
+ towedModData["towBarChangedOffsetZShift"] = zShift
+ towedVehicle:transmitModData()
+ towingVehicle:transmitModData()
+end
+
+function TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
+ local towingModData = towingVehicle:getModData()
+ local towingAttachmentId = towingModData["towBarOriginalTowingAttachmentId"]
+ or towingVehicle:getTowAttachmentSelf()
+ local towingAttachment = towingVehicle:getScript():getAttachmentById(towingAttachmentId)
+ if towingAttachment ~= nil then
+ towingAttachment:setUpdateConstraint(true)
+ local zOffset = (towingAttachmentId == "trailer") and -1 or 1
+ towingAttachment:setZOffset(zOffset)
+
+ -- Restore the original offset that was overridden for rigid tow spacing.
+ local originalX = tonumber(towingModData["towBarOriginalTowingOffsetX"])
+ local originalY = tonumber(towingModData["towBarOriginalTowingOffsetY"])
+ 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()
+
+ local towedModData = towedVehicle:getModData()
+ local changedAttachmentId = towedModData["towBarChangedAttachmentId"] or towedVehicle:getTowAttachmentSelf()
+ local towedAttachment = towedVehicle:getScript():getAttachmentById(changedAttachmentId)
+ if towedAttachment ~= nil then
+ towedAttachment:setUpdateConstraint(true)
+ local zOffset = (changedAttachmentId == "trailer") and -1 or 1
+ towedAttachment:setZOffset(zOffset)
+
+ if towedModData["isChangedTowedAttachment"] then
+ local storedBaseX = tonumber(towedModData["towBarBaseAttachmentOffsetX"])
+ local storedBaseY = tonumber(towedModData["towBarBaseAttachmentOffsetY"])
+ local storedBaseZ = tonumber(towedModData["towBarBaseAttachmentOffsetZ"])
+ local hasStoredBase = towedModData["towBarBaseAttachmentId"] == changedAttachmentId
+ and storedBaseX ~= nil and storedBaseY ~= nil and storedBaseZ ~= nil
+
+ if hasStoredBase then
+ towedAttachment:getOffset():set(storedBaseX, storedBaseY, storedBaseZ)
+ else
+ local offset = towedAttachment:getOffset()
+ local storedShift = tonumber(towedModData["towBarChangedOffsetZShift"])
+ if storedShift ~= nil then
+ towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() - storedShift)
+ else
+ local zShift = offset:z() > 0 and -1 or 1
+ towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() + zShift)
+ end
+ end
+ end
+ end
+
+ towedModData["isChangedTowedAttachment"] = false
+ towedModData["towBarChangedAttachmentId"] = nil
+ towedModData["towBarChangedOffsetZShift"] = nil
+ towedModData["towBarBaseAttachmentId"] = nil
+ towedModData["towBarBaseAttachmentOffsetX"] = nil
+ towedModData["towBarBaseAttachmentOffsetY"] = nil
+ towedModData["towBarBaseAttachmentOffsetZ"] = nil
+ towedVehicle:transmitModData()
+end
+
+-----------------------------------------------------------
+
+--- Fix mods that add vehicles without tow attachments
+local function fixTowAttachmentsForOtherVehicleMods()
+ local scriptManager = getScriptManager()
+ local vehicleScripts = scriptManager:getAllVehicleScripts()
+
+ for i = 0, vehicleScripts:size()-1 do
+ local script = vehicleScripts:get(i)
+ local wheelCount = script:getWheelCount()
+
+ local attachHeigtOffset = -0.5
+ if wheelCount > 0 then
+ attachHeigtOffset = script:getWheel(0):getOffset():y() + 0.1
+ end
+
+ if not string.match(string.lower(script:getName()), "trailer") then
+ local trailerAttachment = script:getAttachmentById("trailer")
+ if trailerAttachment == nil then
+ local attach = ModelAttachment.new("trailer")
+ attach:getOffset():set(0, attachHeigtOffset, -script:getPhysicsChassisShape():z()/2 - 0.1)
+ attach:setZOffset(-1)
+ script:addAttachment(attach)
+ end
+
+ local trailerFrontAttachment = script:getAttachmentById("trailerfront")
+ if trailerFrontAttachment == nil then
+ local attach = ModelAttachment.new("trailerfront")
+ attach:getOffset():set(0, attachHeigtOffset, script:getPhysicsChassisShape():z()/2 + 0.1)
+ attach:setZOffset(1)
+ script:addAttachment(attach)
+ end
+ end
+ end
+end
+
+Events.OnGameBoot.Add(fixTowAttachmentsForOtherVehicleMods)
diff --git a/42.20/media/lua/server/BTTow.lua b/42.20/media/lua/server/BTTow.lua
new file mode 100644
index 0000000..e910409
--- /dev/null
+++ b/42.20/media/lua/server/BTTow.lua
@@ -0,0 +1,89 @@
+BTtow = {}
+BTtow.Create = {}
+BTtow.Init = {}
+
+local TowbarVariantSize = 24
+local TowbarNormalStart = 0
+local TowbarLargeStart = 24
+local TowbarMaxIndex = TowbarVariantSize - 1
+local VanillaScaleMin = 1.5
+local VanillaScaleMax = 2.0
+
+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 isVanillaScale(script)
+ local modelScale = getVehicleModelScale(script)
+ if modelScale == nil then
+ return true
+ end
+
+ local configuredMin = TowBarMod and TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMin)
+ local configuredMax = TowBarMod and 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))
+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)
+end
+
+function BTtow.Create.towbar(vehicle, part)
+ if part == nil then return end
+ for j=0, TowbarVariantSize - 1 do
+ part:setModelVisible("towbar" .. j, false)
+ end
+end
+
+function BTtow.Init.towbar(vehicle, part)
+ if part == nil then return end
+ for j=0, TowbarVariantSize - 1 do
+ part:setModelVisible("towbar" .. j, false)
+ end
+ local modData = vehicle:getModData()
+ if modData and modData.towBarModelSwapInProgress then
+ return
+ end
+ if modData and modData["isTowingByTowBar"] and modData["towed"] then
+ local script = vehicle:getScript()
+ if script then
+ local index = getTowbarModelSlot(script)
+ local shouldShowOnThisPart = part:getId() == "towbar"
+ if shouldShowOnThisPart then
+ part:setModelVisible("towbar" .. index, true)
+ end
+ end
+ end
+end
diff --git a/42.20/media/lua/server/Items/TowBarItem_Distributions.lua b/42.20/media/lua/server/Items/TowBarItem_Distributions.lua
new file mode 100644
index 0000000..0023a63
--- /dev/null
+++ b/42.20/media/lua/server/Items/TowBarItem_Distributions.lua
@@ -0,0 +1,71 @@
+require 'Items/ProceduralDistributions'
+require 'Items/SuburbsDistributions'
+require 'Items/Distributions'
+require 'Items/Distribution_BinJunk'
+require 'Items/Distribution_ClosetJunk'
+require 'Items/Distribution_DeskJunk'
+require 'Items/Distribution_ShelfJunk'
+require 'Items/Distribution_CounterJunk'
+require 'Items/Distribution_SideTableJunk'
+require 'Vehicles/VehicleDistributions'
+require 'Vehicles/VehicleDistribution_GloveBoxJunk'
+require 'Vehicles/VehicleDistribution_SeatJunk'
+require 'Vehicles/VehicleDistribution_TrunkJunk'
+
+----------------- TOW BAR -----------------------
+-- Mirror Jack spawn chance into TowBar in container distributions (world + vehicle containers).
+-- Intentionally excludes story-clutter floor placement tables (RandomizedWorldContent/StoryClutter).
+
+local TOWBAR_ITEM_TYPE = "TowBar.TowBar"
+local JACK_ITEM_TYPES = {
+ ["Jack"] = true,
+ ["Base.Jack"] = true,
+}
+
+local function addMissingTowBarsForJack(items)
+ if type(items) ~= "table" then return end
+
+ local jackCountByChance = {}
+ local towBarCountByChance = {}
+
+ for i = 1, #items, 2 do
+ local itemType = items[i]
+ local chance = tonumber(items[i + 1])
+ if type(itemType) == "string" and chance ~= nil then
+ if JACK_ITEM_TYPES[itemType] then
+ jackCountByChance[chance] = (jackCountByChance[chance] or 0) + 1
+ elseif itemType == TOWBAR_ITEM_TYPE then
+ towBarCountByChance[chance] = (towBarCountByChance[chance] or 0) + 1
+ end
+ end
+ end
+
+ for chance, jackCount in pairs(jackCountByChance) do
+ local missing = jackCount - (towBarCountByChance[chance] or 0)
+ for _ = 1, missing do
+ table.insert(items, TOWBAR_ITEM_TYPE)
+ table.insert(items, chance)
+ end
+ end
+end
+
+local function walkContainerDistributions(root, seen)
+ if type(root) ~= "table" or seen[root] then return end
+ seen[root] = true
+
+ for key, value in pairs(root) do
+ if key == "items" and type(value) == "table" then
+ addMissingTowBarsForJack(value)
+ elseif type(value) == "table" then
+ walkContainerDistributions(value, seen)
+ end
+ end
+end
+
+local seen = {}
+walkContainerDistributions(ProceduralDistributions, seen)
+walkContainerDistributions(SuburbsDistributions, seen)
+walkContainerDistributions(Distributions, seen)
+walkContainerDistributions(VehicleDistributions, seen)
+walkContainerDistributions(ClutterTables, seen)
+
diff --git a/42.20/media/lua/server/TowingCommands.lua b/42.20/media/lua/server/TowingCommands.lua
new file mode 100644
index 0000000..21b2cf2
--- /dev/null
+++ b/42.20/media/lua/server/TowingCommands.lua
@@ -0,0 +1,669 @@
+if isClient() then return end
+
+local TowingCommands = {}
+local Commands = {}
+local TowBarItemType = "TowBar.TowBar"
+local SyncDelayTicks = 2
+local SnapshotIntervalTicks = 120
+local BrokenPairAuditIntervalTicks = 5
+local pendingSync = {}
+local snapshotTickCounter = 0
+local brokenPairAuditTickCounter = 0
+local confirmedTowPairs = {}
+local giveTowBar
+
+TowingCommands.wantNoise = getDebug() or false
+
+local noise = function(msg)
+ if TowingCommands.wantNoise then
+ print("TowBarCommands: " .. msg)
+ end
+end
+
+local function queueSync(kind, player, args, reservedTowBar)
+ if not args then return end
+ table.insert(pendingSync, {
+ kind = kind,
+ ticks = SyncDelayTicks,
+ attempts = 0,
+ reservedTowBar = reservedTowBar == true,
+ player = player,
+ args = args
+ })
+end
+
+local function cancelPendingAttach(vehicleA, vehicleB)
+ if not vehicleA or not vehicleB then return end
+
+ local vehicleAId = vehicleA:getId()
+ local vehicleBId = vehicleB:getId()
+ local remaining = {}
+ for i = 1, #pendingSync do
+ local item = pendingSync[i]
+ local itemArgs = item.args or {}
+ local isThisAttach = item.kind == "attach"
+ and itemArgs.vehicleA == vehicleAId
+ and itemArgs.vehicleB == vehicleBId
+ if isThisAttach then
+ -- The reserved item is about to become the single ground drop.
+ item.reservedTowBar = false
+ else
+ table.insert(remaining, item)
+ end
+ end
+ pendingSync = remaining
+end
+
+local function hasPendingAttach(vehicleA, vehicleB)
+ if not vehicleA or not vehicleB then return false end
+
+ local vehicleAId = vehicleA:getId()
+ local vehicleBId = vehicleB:getId()
+ for i = 1, #pendingSync do
+ local item = pendingSync[i]
+ local itemArgs = item.args or {}
+ if item.kind == "attach"
+ and itemArgs.vehicleA == vehicleAId
+ and itemArgs.vehicleB == vehicleBId then
+ return true
+ end
+ end
+ return false
+end
+
+local function resolveAttachmentA(args, vehicleA)
+ if args and args.attachmentA then return args.attachmentA end
+ if vehicleA and vehicleA:getTowAttachmentSelf() then return vehicleA:getTowAttachmentSelf() end
+ return "trailer"
+end
+
+local function resolveAttachmentB(args, vehicleB)
+ if args and args.attachmentB then return args.attachmentB end
+ if vehicleB and vehicleB:getTowAttachmentSelf() then return vehicleB:getTowAttachmentSelf() end
+ return "trailerfront"
+end
+
+local function isLinked(vehicleA, vehicleB)
+ if not vehicleA or not vehicleB then return false end
+ return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA
+end
+
+local function hasAnyTowLink(vehicle)
+ if not vehicle then return false end
+ return vehicle:getVehicleTowing() ~= nil or vehicle:getVehicleTowedBy() ~= nil
+end
+
+local function getTowBarPairKey(vehicleA, vehicleB)
+ if not vehicleA or not vehicleB then return nil end
+ return tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())
+end
+
+local function markTowBarPairConfirmed(vehicleA, vehicleB)
+ local key = getTowBarPairKey(vehicleA, vehicleB)
+ if key then confirmedTowPairs[key] = true end
+end
+
+local function isTowBarPairConfirmed(vehicleA, vehicleB)
+ local key = getTowBarPairKey(vehicleA, vehicleB)
+ return key ~= nil and confirmedTowPairs[key] == true
+end
+
+local function forgetTowBarPair(vehicleA, vehicleB)
+ local key = getTowBarPairKey(vehicleA, vehicleB)
+ if key then confirmedTowPairs[key] = nil end
+end
+
+local function hasTowBarState(vehicle)
+ if not vehicle then return false end
+ local md = vehicle:getModData()
+ if not md then return false end
+ return md["isTowingByTowBar"] == true
+end
+
+local function markExpectedTowBarPair(vehicleA, vehicleB, attachmentA, attachmentB)
+ if not vehicleA or not vehicleB then return end
+
+ local towingModData = vehicleA:getModData()
+ local towedModData = vehicleB:getModData()
+ if not towingModData or not towedModData then return end
+
+ towingModData["isTowingByTowBar"] = true
+ towingModData["towed"] = false
+ towingModData["towBarTowedVehicleId"] = vehicleB:getId()
+ towingModData["towBarTowingVehicleId"] = nil
+ towingModData["towBarExpectedAttachment"] = attachmentA
+ towedModData["isTowingByTowBar"] = true
+ towedModData["towed"] = true
+ towedModData["towBarTowedVehicleId"] = nil
+ towedModData["towBarTowingVehicleId"] = vehicleA:getId()
+ towedModData["towBarExpectedAttachment"] = attachmentB
+ vehicleA:transmitModData()
+ vehicleB:transmitModData()
+end
+
+local function clearExpectedTowBarPair(vehicleA, vehicleB)
+ if vehicleA then
+ local towingModData = vehicleA:getModData()
+ if towingModData then
+ towingModData["isTowingByTowBar"] = false
+ towingModData["towed"] = false
+ towingModData["towBarTowedVehicleId"] = nil
+ towingModData["towBarTowingVehicleId"] = nil
+ towingModData["towBarExpectedAttachment"] = nil
+ vehicleA:transmitModData()
+ end
+ end
+
+ if vehicleB then
+ local towedModData = vehicleB:getModData()
+ if towedModData then
+ towedModData["isTowingByTowBar"] = false
+ towedModData["towed"] = false
+ towedModData["towBarTowedVehicleId"] = nil
+ towedModData["towBarTowingVehicleId"] = nil
+ towedModData["towBarExpectedAttachment"] = nil
+ vehicleB:transmitModData()
+ end
+ end
+end
+
+local function isExpectedTowBarPair(vehicleA, vehicleB)
+ if not vehicleA or not vehicleB then return false end
+
+ local towingModData = vehicleA:getModData()
+ local towedModData = vehicleB:getModData()
+ if not towingModData or not towedModData then return false end
+
+ return towingModData["towBarTowedVehicleId"] == vehicleB:getId()
+ and towedModData["towBarTowingVehicleId"] == vehicleA:getId()
+end
+
+local function isLegacyTowBarPair(vehicleA, vehicleB)
+ if not vehicleA or not vehicleB then return false end
+
+ local towingModData = vehicleA:getModData()
+ local towedModData = vehicleB:getModData()
+ if not towingModData or not towedModData then return false end
+
+ return hasTowBarState(vehicleA) and hasTowBarState(vehicleB)
+ and towedModData["towed"] == true
+ and towingModData["towBarTowedVehicleId"] == nil
+ and towingModData["towBarTowingVehicleId"] == nil
+ and towedModData["towBarTowedVehicleId"] == nil
+ and towedModData["towBarTowingVehicleId"] == nil
+end
+
+local function resolveExpectedTowBarPair(vehicle)
+ if not vehicle then return nil, nil end
+
+ local towingVehicle = vehicle:getVehicleTowing() and vehicle or vehicle:getVehicleTowedBy()
+ local towedVehicle = vehicle:getVehicleTowing() or (vehicle:getVehicleTowedBy() and vehicle or nil)
+ if towingVehicle and towedVehicle
+ and (isExpectedTowBarPair(towingVehicle, towedVehicle)
+ or isLegacyTowBarPair(towingVehicle, towedVehicle)) then
+ return towingVehicle, towedVehicle
+ end
+
+ local modData = vehicle:getModData()
+ if not modData then return nil, nil end
+
+ if modData["towBarTowedVehicleId"] ~= nil then
+ local towedVehicle = getVehicleById(modData["towBarTowedVehicleId"])
+ if isExpectedTowBarPair(vehicle, towedVehicle) then
+ return vehicle, towedVehicle
+ end
+ end
+ if modData["towBarTowingVehicleId"] ~= nil then
+ local towingVehicle = getVehicleById(modData["towBarTowingVehicleId"])
+ if isExpectedTowBarPair(towingVehicle, vehicle) then
+ return towingVehicle, vehicle
+ end
+ end
+
+ return nil, nil
+end
+
+local function isPlayerAuthorizedForPair(player, vehicleA, vehicleB)
+ if not player or not vehicleA or not vehicleB then return false end
+
+ local playerVehicle = player:getVehicle()
+ if playerVehicle == vehicleA or playerVehicle == vehicleB then
+ return true
+ end
+
+ local playerX = player:getX()
+ local playerY = player:getY()
+ local function isNear(vehicle)
+ local deltaX = playerX - vehicle:getX()
+ local deltaY = playerY - vehicle:getY()
+ return deltaX * deltaX + deltaY * deltaY <= 64
+ end
+
+ return isNear(vehicleA) or isNear(vehicleB)
+end
+
+local function findTowBarItem(player, itemId)
+ if not player then return nil end
+ local inventory = player:getInventory()
+ if not inventory then return nil end
+
+ local towBarItem = itemId and inventory:getItemWithID(itemId) or nil
+ if towBarItem and towBarItem:getFullType() ~= TowBarItemType then
+ towBarItem = nil
+ end
+ return towBarItem or inventory:getFirstTypeRecurse(TowBarItemType)
+end
+
+local function removeTowBarItem(player, towBarItem)
+ if not player or not towBarItem then return false end
+ local inventory = player:getInventory()
+ if not inventory or not inventory:contains(towBarItem, true) then return false end
+
+ local wasPrimary = player:isPrimaryHandItem(towBarItem)
+ local wasSecondary = player:isSecondaryHandItem(towBarItem)
+ player:removeFromHands(towBarItem)
+ inventory:Remove(towBarItem)
+ sendRemoveItemFromContainer(inventory, towBarItem)
+
+ if wasPrimary or wasSecondary then
+ sendEquip(player)
+ end
+ return true
+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 broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
+ if not vehicleA or not vehicleB then return end
+ sendServerCommand("towbar", "forceAttachSync", {
+ vehicleA = vehicleA:getId(),
+ vehicleB = vehicleB:getId(),
+ attachmentA = attachmentA,
+ attachmentB = attachmentB
+ })
+end
+
+local function broadcastDetach(vehicleAId, vehicleBId)
+ sendServerCommand("towbar", "forceDetachSync", {
+ vehicleA = vehicleAId,
+ vehicleB = vehicleBId
+ })
+end
+
+local function broadcastSpontaneousDetach(vehicleA, vehicleB)
+ if not vehicleA or not vehicleB then return end
+ sendServerCommand("towbar", "spontaneousDetachSync", {
+ vehicleA = vehicleA:getId(),
+ vehicleB = vehicleB:getId()
+ })
+end
+
+local function breakTowBarConstraint(towingVehicle, towedVehicle)
+ if not towingVehicle or not towedVehicle then return end
+
+ -- Only break a relation between this exact pair. Stale towbar metadata must
+ -- never disconnect either vehicle from a different trailer.
+ local towingReferencesTowed = towingVehicle:getVehicleTowing() == towedVehicle
+ or towingVehicle:getVehicleTowedBy() == towedVehicle
+ local towedReferencesTowing = towedVehicle:getVehicleTowing() == towingVehicle
+ or towedVehicle:getVehicleTowedBy() == towingVehicle
+ if towedReferencesTowing then
+ towedVehicle:breakConstraint(true, false)
+ end
+ if towingReferencesTowed then
+ towingVehicle:breakConstraint(true, false)
+ end
+end
+
+local function dropTowBarOnGround(towingVehicle, towedVehicle)
+ local square = towedVehicle and towedVehicle:getSquare() or nil
+ if not square and towingVehicle then
+ square = towingVehicle:getSquare()
+ end
+ if not square then
+ noise("could not drop broken towbar because neither vehicle has a square")
+ return false
+ end
+
+ local item = square:AddWorldInventoryItem(TowBarItemType, 0.5, 0.5, 0)
+ if not item then
+ noise("failed to drop broken towbar on the ground")
+ return false
+ end
+ return true
+end
+
+local function finalizeBrokenTowBarPair(towingVehicle, towedVehicle, reason)
+ if not towingVehicle or not towedVehicle then return false end
+ if not isExpectedTowBarPair(towingVehicle, towedVehicle)
+ and not isLegacyTowBarPair(towingVehicle, towedVehicle) then
+ return false
+ end
+
+ local shouldDrop = hasTowBarState(towingVehicle) or hasTowBarState(towedVehicle)
+ if shouldDrop and not dropTowBarOnGround(towingVehicle, towedVehicle) then
+ -- Keep the reciprocal IDs intact so the audit can retry when a square is available.
+ return false
+ end
+
+ cancelPendingAttach(towingVehicle, towedVehicle)
+ forgetTowBarPair(towingVehicle, towedVehicle)
+ breakTowBarConstraint(towingVehicle, towedVehicle)
+ clearExpectedTowBarPair(towingVehicle, towedVehicle)
+
+ if isServer() then
+ broadcastSpontaneousDetach(towingVehicle, towedVehicle)
+ elseif not isClient() and TowBarMod and TowBarMod.Hook
+ and TowBarMod.Hook.cleanupDetachedTowBar then
+ local ok, err = pcall(TowBarMod.Hook.cleanupDetachedTowBar, towingVehicle, towedVehicle)
+ if not ok then
+ noise("single-player broken towbar cleanup failed: " .. tostring(err))
+ return false
+ end
+ end
+
+ noise("finalized broken towbar pair " .. tostring(towingVehicle:getId())
+ .. ":" .. tostring(towedVehicle:getId()) .. " reason=" .. tostring(reason)
+ .. " dropped=" .. tostring(shouldDrop))
+ return true
+end
+
+local function reconcileBrokenTowBarPairsServer()
+ brokenPairAuditTickCounter = brokenPairAuditTickCounter + 1
+ if brokenPairAuditTickCounter < BrokenPairAuditIntervalTicks then return end
+ brokenPairAuditTickCounter = 0
+
+ local cell = getCell()
+ if not cell then return end
+ local vehicles = cell:getVehicles()
+ if not vehicles then return end
+
+ forEachCollectionItem(vehicles, function(towingVehicle)
+ local towingModData = towingVehicle and towingVehicle:getModData() or nil
+ local towedVehicleId = towingModData and towingModData["towBarTowedVehicleId"] or nil
+ local towedVehicle = towedVehicleId and getVehicleById(towedVehicleId) or nil
+ if towingVehicle and towedVehicle and isExpectedTowBarPair(towingVehicle, towedVehicle) then
+ if isLinked(towingVehicle, towedVehicle) then
+ markTowBarPairConfirmed(towingVehicle, towedVehicle)
+ elseif isTowBarPairConfirmed(towingVehicle, towedVehicle)
+ and not hasAnyTowLink(towingVehicle)
+ and not hasAnyTowLink(towedVehicle) then
+ finalizeBrokenTowBarPair(towingVehicle, towedVehicle, "physical-link-audit")
+ end
+ end
+ end)
+end
+
+local function processAttachSync(item)
+ local args = item.args or {}
+ local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil
+ local vehicleB = args.vehicleB and getVehicleById(args.vehicleB) or nil
+ if not vehicleA or not vehicleB then
+ noise("attach sync skipped missing vehicles A=" .. tostring(args.vehicleA) .. " B=" .. tostring(args.vehicleB))
+ return "failed"
+ end
+
+ local attachmentA = resolveAttachmentA(args, vehicleA)
+ local attachmentB = resolveAttachmentB(args, vehicleB)
+ if not isLinked(vehicleA, vehicleB) then
+ if isTowBarPairConfirmed(vehicleA, vehicleB) then return "broken" end
+ if item.attempts >= 3 then return "failed" end
+ vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB)
+ return "retry"
+ end
+
+ markExpectedTowBarPair(vehicleA, vehicleB, attachmentA, attachmentB)
+ markTowBarPairConfirmed(vehicleA, vehicleB)
+ broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
+ return "complete"
+end
+
+local function failAttachSync(item)
+ local args = item.args or {}
+ local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil
+ local vehicleB = args.vehicleB and getVehicleById(args.vehicleB) or nil
+
+ if isLinked(vehicleA, vehicleB) then
+ vehicleA:breakConstraint(true, false)
+ end
+ clearExpectedTowBarPair(vehicleA, vehicleB)
+ broadcastDetach(args.vehicleA, args.vehicleB)
+
+ if item.reservedTowBar then
+ giveTowBar(item.player, true)
+ item.reservedTowBar = false
+ end
+end
+
+local function processDetachSync(item)
+ local args = item.args or {}
+ local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle
+ local vehicleBId = args.vehicleB or args.vehicle
+ broadcastDetach(vehicleAId, vehicleBId)
+end
+
+local function snapshotActiveTowbarLinksServer()
+ local cell = getCell()
+ if not cell then return end
+ local list = cell:getVehicles()
+ if not list then return end
+
+ forEachCollectionItem(list, function(towingVehicle)
+ local towedVehicle = towingVehicle and towingVehicle:getVehicleTowing() or nil
+ if towingVehicle and towedVehicle and towedVehicle:getVehicleTowedBy() == towingVehicle then
+ -- Preserve reciprocal modern pairs and clean legacy pairs only.
+ -- Stale modern IDs must not promote an unrelated current trailer.
+ if isExpectedTowBarPair(towingVehicle, towedVehicle)
+ or isLegacyTowBarPair(towingVehicle, towedVehicle) then
+ local attachmentA = resolveAttachmentA(nil, towingVehicle)
+ local towedMd = towedVehicle:getModData()
+ local attachmentB = (towedMd and towedMd["towBarChangedAttachmentId"]) or resolveAttachmentB(nil, towedVehicle)
+ if attachmentA == attachmentB then
+ attachmentA = "trailer"
+ attachmentB = "trailerfront"
+ end
+ markExpectedTowBarPair(towingVehicle, towedVehicle, attachmentA, attachmentB)
+ markTowBarPairConfirmed(towingVehicle, towedVehicle)
+ broadcastAttach(towingVehicle, towedVehicle, attachmentA, attachmentB)
+ end
+ end
+ end)
+end
+
+local function processPendingSync()
+ reconcileBrokenTowBarPairsServer()
+
+ snapshotTickCounter = snapshotTickCounter + 1
+ if snapshotTickCounter >= SnapshotIntervalTicks then
+ snapshotTickCounter = 0
+ snapshotActiveTowbarLinksServer()
+ end
+
+ if #pendingSync == 0 then return end
+
+ local remaining = {}
+ for i = 1, #pendingSync do
+ local item = pendingSync[i]
+ item.ticks = item.ticks - 1
+ if item.ticks <= 0 then
+ if item.kind == "attach" then
+ local status = processAttachSync(item)
+ if status == "retry" and item.attempts < 3 then
+ item.attempts = item.attempts + 1
+ item.ticks = SyncDelayTicks
+ table.insert(remaining, item)
+ elseif status == "broken" then
+ finalizeBrokenTowBarPair(
+ item.args.vehicleA and getVehicleById(item.args.vehicleA) or nil,
+ item.args.vehicleB and getVehicleById(item.args.vehicleB) or nil,
+ "attach-confirmation-break"
+ )
+ elseif status ~= "complete" then
+ failAttachSync(item)
+ end
+ elseif item.kind == "detach" then
+ processDetachSync(item)
+ end
+ else
+ table.insert(remaining, item)
+ end
+ end
+ pendingSync = remaining
+end
+
+function Commands.attachTowBar(player, args)
+ if not player or type(args) ~= "table" then return end
+ if type(args.attachmentA) ~= "string" or type(args.attachmentB) ~= "string" then return end
+
+ local vehicleA = getVehicleById(args.vehicleA)
+ local vehicleB = getVehicleById(args.vehicleB)
+ if not vehicleA then
+ noise("no such vehicle (A) id=" .. tostring(args.vehicleA))
+ return
+ end
+ if not vehicleB then
+ noise("no such vehicle (B) id=" .. tostring(args.vehicleB))
+ return
+ end
+ if vehicleA == vehicleB or not isPlayerAuthorizedForPair(player, vehicleA, vehicleB) then
+ noise("rejected invalid or remote towbar attach")
+ return
+ end
+ if hasAnyTowLink(vehicleA) or hasAnyTowLink(vehicleB) then
+ noise("rejected conflicting towbar attach")
+ return
+ end
+ if isExpectedTowBarPair(vehicleA, vehicleB)
+ or isExpectedTowBarPair(vehicleB, vehicleA)
+ or hasPendingAttach(vehicleA, vehicleB)
+ or hasPendingAttach(vehicleB, vehicleA) then
+ noise("rejected duplicate pending towbar attach")
+ return
+ end
+
+ local towBarItem = findTowBarItem(player, args.itemId)
+ if not towBarItem then
+ noise("rejected towbar attach without item")
+ return
+ end
+ if not removeTowBarItem(player, towBarItem) then
+ noise("rejected towbar attach because item reservation failed")
+ return
+ end
+
+ -- Constraint creation may complete on a later server tick in multiplayer.
+ -- Record and queue the reserved item first so an immediate Build 42.20
+ -- spontaneous break can resolve the pair and consume the reservation once.
+ markExpectedTowBarPair(vehicleA, vehicleB, args.attachmentA, args.attachmentB)
+ queueSync("attach", player, args, true)
+ vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
+ if isLinked(vehicleA, vehicleB) then
+ markTowBarPairConfirmed(vehicleA, vehicleB)
+ end
+end
+
+function Commands.detachTowBar(player, args)
+ local towingVehicle = args.towingVehicle and getVehicleById(args.towingVehicle) or nil
+ local towedVehicle = args.vehicle and getVehicleById(args.vehicle) or nil
+
+ if not towingVehicle and towedVehicle then
+ towingVehicle = towedVehicle:getVehicleTowedBy()
+ end
+ if not towedVehicle and towingVehicle then
+ towedVehicle = towingVehicle:getVehicleTowing()
+ end
+ if not isPlayerAuthorizedForPair(player, towingVehicle, towedVehicle) then
+ noise("rejected unauthorized towbar detach")
+ return
+ end
+ if not isLinked(towingVehicle, towedVehicle) then
+ noise("rejected mismatched or stale towbar detach")
+ return
+ end
+ if not isExpectedTowBarPair(towingVehicle, towedVehicle)
+ and not isLegacyTowBarPair(towingVehicle, towedVehicle) then
+ noise("rejected detach for a non-towbar or stale linked pair")
+ return
+ end
+
+ local shouldRefund = hasTowBarState(towingVehicle) or hasTowBarState(towedVehicle)
+
+ breakTowBarConstraint(towingVehicle, towedVehicle)
+ forgetTowBarPair(towingVehicle, towedVehicle)
+ clearExpectedTowBarPair(towingVehicle, towedVehicle)
+ queueSync("detach", player, args)
+ if shouldRefund then
+ giveTowBar(player, true)
+ end
+end
+
+function Commands.consumeTowBar(player, args)
+ local itemId = args and args.itemId
+ removeTowBarItem(player, findTowBarItem(player, itemId))
+end
+
+giveTowBar = function(player, equipPrimary)
+ if not player then return end
+ local inventory = player:getInventory()
+ if not inventory then return end
+
+ local towBarItem = inventory:AddItem(TowBarItemType)
+ if not towBarItem then return end
+ sendAddItemToContainer(inventory, towBarItem)
+
+ if equipPrimary then
+ player:setPrimaryHandItem(towBarItem)
+ sendEquip(player)
+ end
+end
+
+-- Compatibility aliases for older command names.
+Commands.attachConstraint = Commands.attachTowBar
+Commands.detachConstraint = Commands.detachTowBar
+
+TowingCommands.OnClientCommand = function(module, command, player, args)
+ if module == "vehicle" and command == "detachTrailerSpontaneous" then
+ args = args or {}
+ local vehicle = args.vehicle and getVehicleById(args.vehicle) or nil
+ local towingVehicle, towedVehicle = resolveExpectedTowBarPair(vehicle)
+ if towingVehicle and towedVehicle then
+ finalizeBrokenTowBarPair(towingVehicle, towedVehicle, "detachTrailerSpontaneous")
+ end
+ return
+ end
+
+ if module == "towbar" and Commands[command] then
+ local argStr = ""
+ args = args or {}
+ for k, v in pairs(args) do
+ argStr = argStr .. " " .. tostring(k) .. "=" .. tostring(v)
+ end
+ noise("received " .. module .. " " .. command .. " " .. tostring(player) .. argStr)
+ Commands[command](player, args)
+ end
+end
+
+Events.OnClientCommand.Add(TowingCommands.OnClientCommand)
+Events.OnTick.Add(processPendingSync)
diff --git a/42.20/media/lua/shared/Translate/EN/IG_UI.json b/42.20/media/lua/shared/Translate/EN/IG_UI.json
new file mode 100644
index 0000000..c2decbb
--- /dev/null
+++ b/42.20/media/lua/shared/Translate/EN/IG_UI.json
@@ -0,0 +1,3 @@
+{
+ "IGUI_VehicleParttowbar": "Towbar Attachment"
+}
diff --git a/42.20/media/lua/shared/Translate/EN/ItemName.json b/42.20/media/lua/shared/Translate/EN/ItemName.json
new file mode 100644
index 0000000..c5abe93
--- /dev/null
+++ b/42.20/media/lua/shared/Translate/EN/ItemName.json
@@ -0,0 +1,3 @@
+{
+ "ItemName_TowBar.TowBar": "Tow Bar"
+}
diff --git a/42.20/media/lua/shared/Translate/EN/Tooltip.json b/42.20/media/lua/shared/Translate/EN/Tooltip.json
new file mode 100644
index 0000000..f0e670a
--- /dev/null
+++ b/42.20/media/lua/shared/Translate/EN/Tooltip.json
@@ -0,0 +1,3 @@
+{
+ "Tooltip_TowBar": "A tow bar can be used
to tow vehicles like a trailer"
+}
diff --git a/42.20/media/lua/shared/Translate/EN/UI.json b/42.20/media/lua/shared/Translate/EN/UI.json
new file mode 100644
index 0000000..2e33852
--- /dev/null
+++ b/42.20/media/lua/shared/Translate/EN/UI.json
@@ -0,0 +1,17 @@
+{
+ "UI_Text_Towing_turnOffParkingBrake": "Turn off parking brake",
+ "UI_Text_Towing_turnOnParkingBrake": "Turn on parking brake",
+ "UI_Text_Towing_noAviableVehicles": "No vehicles for attach
or can't attach",
+ "UI_Text_Towing_attach": "Attach",
+ "UI_Text_Towing_deattach": "Deattach",
+ "UI_Text_Towing_byRope": "by rope",
+ "UI_Text_Towing_byTowBar": "by tow bar",
+ "UI_Text_Towing_byHook": "by hook",
+ "UI_Text_Towing_flipUpright": "Flip upright",
+ "UI_Text_Towing_cannotDriveWhileTowed": "Cannot drive while being towed",
+ "UI_Text_PushByHands": "Push vehicle",
+ "UI_Text_PushByHands_Left": "Left",
+ "UI_Text_PushByHands_Right": "Right",
+ "UI_Text_PushByHands_Front": "Front",
+ "UI_Text_PushByHands_Behind": "Behind"
+}
diff --git a/42.20/media/registries.lua b/42.20/media/registries.lua
new file mode 100644
index 0000000..ea87939
--- /dev/null
+++ b/42.20/media/registries.lua
@@ -0,0 +1,3 @@
+-- Build 42 registry file.
+-- This mod currently uses only base registries (for example ItemType = base:normal),
+-- so no custom identifier registrations are required here.
diff --git a/42.20/media/scripts/TowBar_items.txt b/42.20/media/scripts/TowBar_items.txt
new file mode 100644
index 0000000..554d51c
--- /dev/null
+++ b/42.20/media/scripts/TowBar_items.txt
@@ -0,0 +1,15 @@
+module TowBar
+{
+/*******************Towing Car*******************/
+ item TowBar
+ {
+ DisplayCategory = Tool,
+ Weight = 8.0,
+ ItemType = base:normal,
+ Icon = TowBar,
+ Tooltip = Tooltip_TowBar,
+ StaticModel = towbarModel,
+ WorldStaticModel = towbarModel,
+ }
+}
+
diff --git a/42.20/media/scripts/vehicles/burntvehicles.txt b/42.20/media/scripts/vehicles/burntvehicles.txt
new file mode 100644
index 0000000..4128178
--- /dev/null
+++ b/42.20/media/scripts/vehicles/burntvehicles.txt
@@ -0,0 +1,3265 @@
+module Base
+{
+ model Vehicles_PickUpBurnt
+ {
+ mesh = vehicles/Vehicles_PickUpBurnt,
+ shader = vehicle,
+ static = TRUE,
+ scale = 0.01,
+ }
+ vehicle PickupBurnt
+ {
+ model
+ {
+ file = Vehicles_PickupBurnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3462 0.0000,
+ }
+
+ spawnOffsetY = 0.24999994,
+
+ skin
+ {
+ texture = Vehicles/vehicles_pickupburnt,
+ }
+
+ extents = 0.8022 0.7033 2.1868,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.3462 0.0000,
+ physicsChassisShape = 0.8022 0.7033 2.1868,
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3352 -1.1648,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3626 1.1374,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_PickUpVanBurnt
+ {
+ mesh = vehicles/Vehicles_PickUpVanBurnt,
+ shader = vehicle,
+ scale = 0.01,
+ }
+ vehicle PickUpVanBurnt
+ {
+ model
+ {
+ file = Vehicles_PickUpVanBurnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3516 0.0000,
+ }
+
+ spawnOffsetY = 0.24999994,
+
+ skin
+ {
+ texture = Vehicles/vehicles_pickupvanburnt,
+ }
+
+ extents = 0.6813 0.6374 2.1868,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.3297 0.0000,
+ physicsChassisShape = 0.6813 0.6374 2.1868,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3516 -1.1978,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3352 1.1758,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_CarNormal_Burnt
+ {
+ mesh = vehicles/Vehicles_CarNormal_Burnt,
+ shader = vehicle,
+ scale = 0.01,
+ }
+ vehicle CarNormalBurnt
+ {
+ model
+ {
+ file = Vehicles_CarNormal_Burnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3297 0.0000,
+ }
+
+ spawnOffsetY = 0.19999991,
+
+ skin
+ {
+ texture = Vehicles/vehicles_carnormal_burnt,
+ }
+
+ extents = 0.8022 0.6593 2.6044,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.3242 0.0000,
+ physicsChassisShape = 0.8022 0.6593 2.6044,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.2637 -1.3242,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3352 1.3297,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ vehicle TaxiBurnt
+ {
+ model
+ {
+ file = Vehicles_CarNormal_Burnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3297 0.0000,
+ }
+
+ spawnOffsetY = 0.19999991,
+
+ skin
+ {
+ texture = Vehicles/vehicles_taxi_burnt,
+ }
+
+ extents = 0.9011 0.6593 2.5055,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.3297 0.0000,
+ physicsChassisShape = 0.9011 0.6593 2.5055,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.1923 -1.3736,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3242 1.3462,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_ModernCar02Burnt
+ {
+ mesh = vehicles/Vehicles_ModernCar02_Burnt,
+ shader = vehicle,
+ scale = 0.01,
+ }
+ vehicle ModernCar02Burnt
+ {
+ model
+ {
+ file = Vehicles_ModernCarBurnt,
+ scale = 1.8200,
+ offset = 0.0000 0.2912 0.0000,
+ }
+
+ spawnOffsetY = 0.099999994,
+
+ skin
+ {
+ texture = Vehicles/vehicles_moderncar02_burnt,
+ }
+
+ extents = 0.6813 0.6374 2.1868,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.2912 0.0000,
+ physicsChassisShape = 0.6813 0.6374 2.1868,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.2802 -1.1538,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3242 1.1978,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_ModernCarBurnt
+ {
+ mesh = vehicles/Vehicles_ModernCarBurnt,
+ shader = vehicle,
+ scale = 0.01,
+ }
+ vehicle ModernCarBurnt
+ {
+ model
+ {
+ file = Vehicles_ModernCarBurnt,
+ scale = 1.8200,
+ offset = 0.0000 0.2912 0.0000,
+ }
+
+ spawnOffsetY = 0.099999994,
+
+ skin
+ {
+ texture = Vehicles/vehicles_moderncar_burnt,
+ }
+
+ extents = 1.0000 0.5604 2.1868,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.2912 0.0000,
+ physicsChassisShape = 1.0000 0.5604 2.1868,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.2857 -1.1484,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3077 1.1758,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_SportsCar_Burnt
+ {
+ mesh = vehicles/Vehicles_SportsCar_Burnt,
+ shader = vehicle,
+ static = TRUE,
+ scale = 0.01,
+ }
+ vehicle SportsCarBurnt
+ {
+ model
+ {
+ file = Vehicles_SportsCar_Burnt,
+ scale = 1.8200,
+ offset = 0.0000 0.0000 0.0000,
+ }
+
+ spawnOffsetY = -0.20000005,
+
+ skin
+ {
+ texture = Vehicles/vehicles_sportscar_burnt,
+ }
+
+ extents = 0.8022 0.4725 2.1868,
+ mass = 400,
+ centerOfMassOffset = 0.0000 0.0000 0.0000,
+ physicsChassisShape = 0.8022 0.4725 2.1868,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.1923 -1.0549,
+ rotate = 0.0000 0.0000 0.0000,
+ zoffset = -1,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.2198 1.0604,
+ rotate = 0.0000 0.0000 0.0000,
+ zoffset = 1,
+ }
+ }
+ model Vehicles_SmallCar02Burnt
+ {
+ mesh = vehicles/Vehicles_SmallCar02Burnt,
+ shader = vehicle,
+ scale = 0.01,
+ }
+ vehicle SmallCar02Burnt
+ {
+ model
+ {
+ file = Vehicles_SmallCar02Burnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3352 0.0000,
+ }
+
+ spawnOffsetY = 0.19999991,
+
+ skin
+ {
+ texture = Vehicles/vehicles_smallcar02_burnt,
+ }
+
+ extents = 0.8022 0.6703 2.1868,
+ mass = 400,
+ centerOfMassOffset = 0.0000 0.3352 0.0000,
+ physicsChassisShape = 0.8022 0.6703 2.1868,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3352 -1.0714,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.2967 1.1099,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_SmallCar_Burnt
+ {
+ mesh = vehicles/Vehicles_SmallCar_Burnt,
+ shader = vehicle,
+ static = TRUE,
+ scale = 0.01,
+ }
+ vehicle SmallCarBurnt
+ {
+ model
+ {
+ file = Vehicles_SmallCar_Burnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3022 0.0000,
+ }
+
+ spawnOffsetY = -0.10000008,
+
+ skin
+ {
+ texture = Vehicles/vehicles_smallcar_burnt,
+ }
+
+ extents = 0.7473 0.6044 1.8571,
+ mass = 400,
+ centerOfMassOffset = 0.0000 0.3022 0.0000,
+ physicsChassisShape = 0.7473 0.6044 1.8571,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.2418 0.6264,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.2088 -0.9231,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_VanSeats_Burnt
+ {
+ mesh = vehicles/Vehicles_VanSeats_Burnt,
+ shader = vehicle,
+ static = TRUE,
+ scale = 0.01,
+ }
+ vehicle VanSeatsBurnt
+ {
+ model
+ {
+ file = Vehicles_VanSeats_Burnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3626 0.0000,
+ }
+
+ spawnOffsetY = 0.5499999,
+
+ skin
+ {
+ texture = Vehicles/vehicles_vanseats_burnt,
+ }
+
+ physicsChassisShape = 0.9341 0.7253 2.3297,
+ centerOfMassOffset = 0.0000 0.3626 0.0000,
+ mass = 500,
+ extents = 0.9341 0.7253 2.3297,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3352 -1.2363,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3352 1.2253,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_Van_Burnt
+ {
+ mesh = vehicles/Vehicles_Van_Burnt,
+ shader = vehicle,
+ static = TRUE,
+ scale = 0.01,
+ }
+ vehicle VanBurnt
+ {
+ model
+ {
+ file = Vehicles_Van_Burnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3462 0.0000,
+ }
+
+ spawnOffsetY = 0.5499999,
+
+ skin
+ {
+ texture = Vehicles/vehicles_van_burnt,
+ }
+
+ physicsChassisShape = 0.9341 0.7253 2.3297,
+ centerOfMassOffset = 0.0000 0.3626 0.0000,
+ mass = 500,
+ extents = 0.9341 0.7253 2.3297,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3352 -1.2692,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3462 1.2198,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_SUV_Burnt
+ {
+ mesh = vehicles/Vehicles_SUV_Burnt,
+ shader = vehicle,
+ scale = 0.01,
+ }
+ vehicle SUVBurnt
+ {
+ model
+ {
+ file = Vehicles_SUV_Burnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3297 0.0000,
+ }
+
+ spawnOffsetY = 0.24999978,
+
+ skin
+ {
+ texture = Vehicles/vehicles_suv_burnt,
+ }
+
+ extents = 0.9011 0.6593 2.1868,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.3297 0.0000,
+ physicsChassisShape = 0.9011 0.6593 2.1868,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3297 -1.0989,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3297 1.0989,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_OffRoadBurnt
+ {
+ mesh = vehicles/Vehicles_OffRoadBurnt,
+ shader = vehicle,
+ scale = 0.01,
+ }
+ vehicle OffRoadBurnt
+ {
+ model
+ {
+ file = Vehicles_OffRoadBurnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3407 0.0000,
+ }
+
+ spawnOffsetY = 0.24999978,
+
+ skin
+ {
+ texture = Vehicles/vehicles_offroad_burnt,
+ }
+
+ extents = 0.8022 0.7143 1.7473,
+ mass = 400,
+ centerOfMassOffset = 0.0000 0.3571 0.0000,
+ physicsChassisShape = 0.8022 0.7143 1.7473,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3846 -0.8846,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3462 0.9286,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_LuxuryCarBurnt
+ {
+ mesh = vehicles/Vehicles_LuxuryCarBurnt,
+ shader = vehicle,
+ scale = 0.01,
+ }
+ vehicle LuxuryCarBurnt
+ {
+ model
+ {
+ file = Vehicles_LuxuryCarBurnt,
+ scale = 1.6200,
+ offset = 0.0000 0.3889 0.0000,
+ }
+
+ spawnOffsetY = 0.19999991,
+
+ skin
+ {
+ texture = Vehicles/vehicles_luxurycar_burnt,
+ }
+
+ extents = 1.0988 0.8025 2.8025,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.3889 0.0000,
+ physicsChassisShape = 1.0988 0.8025 2.8025,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3333 -1.4815,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3765 1.5185,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_PickUpVanLightsBurnt
+ {
+ mesh = vehicles/Vehicles_PickUpVanLightsBurnt,
+ shader = vehicle,
+ static = TRUE,
+ scale = 0.01,
+ }
+ vehicle PickUpVanLightsBurnt
+ {
+ model
+ {
+ file = Vehicles_PickUpVanLightsBurnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3407 0.0000,
+ }
+
+ spawnOffsetY = 0.24999994,
+
+ skin
+ {
+ texture = Vehicles/vehicles_pickupvanlightsburnt,
+ }
+
+ extents = 0.6813 0.7033 2.1868,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.3407 0.0000,
+ physicsChassisShape = 0.6813 0.7033 2.1868,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3297 -1.1758,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3242 1.1868,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_Ambulance_Burnt
+ {
+ mesh = vehicles/Vehicles_Ambulance_Burnt,
+ shader = vehicle,
+ scale = 0.01,
+ }
+ vehicle AmbulanceBurnt
+ {
+ model
+ {
+ file = Vehicles_Ambulance_Burnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3516 0.0000,
+ }
+
+ spawnOffsetY = 0.7999999,
+
+ skin
+ {
+ texture = Vehicles/vehicles_ambulance_burnt,
+ }
+
+ extents = 0.9011 0.8791 2.3077,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.4396 0.0000,
+ physicsChassisShape = 0.9011 0.8791 2.3077,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.4011 -1.2198,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3956 1.2033,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_VanRadio_Burnt
+ {
+ mesh = vehicles/Vehicles_VanRadio_Burnt,
+ shader = vehicle,
+ static = TRUE,
+ scale = 0.01,
+ }
+ vehicle VanRadioBurnt
+ {
+ model
+ {
+ file = Vehicles_VanRadio_Burnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3846 0.0000,
+ }
+
+ spawnOffsetY = 0.7999999,
+
+ skin
+ {
+ texture = Vehicles/vehicles_vanradio_burnt,
+ }
+
+ extents = 0.9011 0.9341 2.3077,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.4670 0.0000,
+ physicsChassisShape = 0.9011 0.9341 2.3077,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3626 -1.2857,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3242 1.2253,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ vehicle PickupSpecialBurnt
+ {
+ model
+ {
+ file = Vehicles_PickupBurnt,
+ scale = 1.8200,
+ offset = 0.0000 0.3462 0.0000,
+ }
+
+ spawnOffsetY = 0.1499999,
+
+ skin
+ {
+ texture = Vehicles/vehicles_pickupburnt_fire,
+ }
+
+ skin
+ {
+ texture = Vehicles/vehicles_pickupburnt_fossoil,
+ }
+
+ skin
+ {
+ texture = Vehicles/vehicles_pickupburnt_police,
+ }
+
+ skin
+ {
+ texture = Vehicles/vehicles_pickupburnt_ranger,
+ }
+
+ extents = 0.8022 0.6593 2.1868,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.3462 0.0000,
+ physicsChassisShape = 0.8022 0.6593 2.1868,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.2967 -1.1648,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3187 1.1374,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+ model Vehicles_NormalCar_BurntPolice
+ {
+ mesh = vehicles/Vehicles_NormalCar_BurntPolice,
+ shader = vehicle,
+ static = TRUE,
+ scale = 0.01,
+ }
+ vehicle NormalCarBurntPolice
+ {
+ model
+ {
+ file = Vehicles_NormalCar_BurntPolice,
+ scale = 1.7000,
+ offset = 0.0000 0.3176 0.0000,
+ }
+
+ spawnOffsetY = 0.64999986,
+
+ skin
+ {
+ texture = Vehicles/vehicles_normalcar_burntpolice,
+ }
+
+ extents = 0.9059 0.6000 2.6000,
+ mass = 500,
+ centerOfMassOffset = 0.0000 0.3176 0.0000,
+ physicsChassisShape = 0.9059 0.6000 2.6000,
+
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ attachment trailer
+ {
+ offset = 0.0000 -0.3353 -1.3059,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+
+ attachment trailerfront
+ {
+ offset = 0.0000 -0.3294 1.3529,
+ rotate = 0.0000 0.0000 0.0000,
+ }
+ }
+}
diff --git a/42.20/media/scripts/vehicles/template_battery.txt b/42.20/media/scripts/vehicles/template_battery.txt
new file mode 100644
index 0000000..d5a679a
--- /dev/null
+++ b/42.20/media/scripts/vehicles/template_battery.txt
@@ -0,0 +1,315 @@
+module Base
+{
+ template vehicle Battery
+ {
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+ part towbarLarge
+ {
+ model towbar0
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+ part Battery
+ {
+ area = Engine,
+ itemType = Base.CarBattery,
+ mechanicRequireKey = true,
+ category = engine,
+ table install
+ {
+ items
+ {
+ 1
+ {
+ type = Base.Screwdriver,
+ count = 1,
+ keep = true,
+ equip = primary,
+ }
+ }
+ time = 100,
+ professions = ,
+ skills = ,
+ traits = ,
+ recipes = ,
+ test = Vehicles.InstallTest.Default,
+ door = EngineDoor,
+ }
+ table uninstall
+ {
+ items
+ {
+ 1
+ {
+ type = Base.Screwdriver,
+ count = 1,
+ keep = true,
+ equip = primary,
+ }
+ }
+ time = 100,
+ test = Vehicles.UninstallTest.Battery,
+ }
+ lua
+ {
+ create = Vehicles.Create.Battery,
+ update = Vehicles.Update.Battery,
+ }
+ }
+ }
+}
diff --git a/42.20/media/scripts/vehicles/template_towbar.txt b/42.20/media/scripts/vehicles/template_towbar.txt
new file mode 100644
index 0000000..68f1b24
--- /dev/null
+++ b/42.20/media/scripts/vehicles/template_towbar.txt
@@ -0,0 +1,284 @@
+module Base
+{
+ model towbarModel
+ {
+ mesh = vehicles/Towbar,
+ texture = Vehicles/Towbar_Texture,
+ scale = 0.01,
+ }
+
+ model towbarModelLarge
+ {
+ mesh = vehicles/Towbar,
+ texture = Vehicles/Towbar_Texture,
+ scale = 0.02022,
+ }
+
+ template vehicle Towbar
+ {
+ part towbar
+ {
+ model towbar0
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModel,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModel,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModel,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ part towbarLarge
+ {
+ model towbar0
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.0,
+ }
+ model towbar1
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.1,
+ }
+ model towbar2
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.2,
+ }
+ model towbar3
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.3,
+ }
+ model towbar4
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.4,
+ }
+ model towbar5
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.5,
+ }
+ model towbar6
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.6,
+ }
+ model towbar7
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.7,
+ }
+ model towbar8
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.8,
+ }
+ model towbar9
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 1.9,
+ }
+ model towbar10
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.0,
+ }
+ model towbar11
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.1,
+ }
+ model towbar12
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.2,
+ }
+ model towbar13
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.3,
+ }
+ model towbar14
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.4,
+ }
+ model towbar15
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.5,
+ }
+ model towbar16
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.6,
+ }
+ model towbar17
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.7,
+ }
+ model towbar18
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.8,
+ }
+ model towbar19
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 2.9,
+ }
+ model towbar20
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 3.0,
+ }
+ model towbar21
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 3.1,
+ }
+ model towbar22
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 3.2,
+ }
+ model towbar23
+ {
+ file = towbarModelLarge,
+ offset = 0 -0.3 3.3,
+ }
+
+ area = Engine,
+ mechanicRequireKey = false,
+ lua
+ {
+ create = BTtow.Create.towbar,
+ init = BTtow.Init.towbar,
+ }
+ }
+
+ }
+}
diff --git a/42.20/mod.info b/42.20/mod.info
new file mode 100644
index 0000000..bda128c
--- /dev/null
+++ b/42.20/mod.info
@@ -0,0 +1,10 @@
+name=Towbars
+id=hrsys_towbars
+poster=../common/media/textures/preview.png
+description=Tow bars for vehicle-to-vehicle towing.
+author=Riggs0
+category=vehicle
+icon=../common/media/textures/tow_bar_icon.png
+url=https://hudsonriggs.systems
+modversion=1.0.5
+versionMin=42.20.0
diff --git a/mod.info b/mod.info
index 09039f1..d0d7f82 100644
--- a/mod.info
+++ b/mod.info
@@ -6,5 +6,5 @@ author=Riggs0
category=vehicle
versionMin=42.13.0
url=https://hudsonriggs.systems
-modversion=1.0.2
+modversion=1.0.5
icon=common/media/textures/tow_bar_icon.png
diff --git a/tests/validate-42.20.ps1 b/tests/validate-42.20.ps1
new file mode 100644
index 0000000..4b38f6d
--- /dev/null
+++ b/tests/validate-42.20.ps1
@@ -0,0 +1,168 @@
+$ErrorActionPreference = "Stop"
+
+$repositoryRoot = Split-Path -Parent $PSScriptRoot
+$releaseRoot = Join-Path $repositoryRoot "42.20"
+$baselineRoot = Join-Path $repositoryRoot "42.18"
+$gameRoot = "D:\SteamLibrary\steamapps\common\ProjectZomboid"
+$failures = [Collections.Generic.List[string]]::new()
+
+function Assert-True {
+ param(
+ [bool]$Condition,
+ [string]$Message
+ )
+
+ if (-not $Condition) {
+ $script:failures.Add($Message)
+ }
+}
+
+function Get-RelativeFileNames {
+ param([string]$Root)
+
+ return Get-ChildItem -LiteralPath $Root -Recurse -File |
+ ForEach-Object { $_.FullName.Substring($Root.Length + 1).Replace("\", "/") } |
+ Sort-Object
+}
+
+Assert-True (Test-Path -LiteralPath $releaseRoot) "Missing 42.20 release folder."
+
+$releaseInfo = Get-Content -LiteralPath (Join-Path $releaseRoot "mod.info")
+$rootInfo = Get-Content -LiteralPath (Join-Path $repositoryRoot "mod.info")
+Assert-True ($releaseInfo -contains "id=hrsys_towbars") "42.20 must use the stable mod id."
+Assert-True ($releaseInfo -contains "versionMin=42.20.0") "42.20 must require Build 42.20."
+Assert-True ($releaseInfo -contains "modversion=1.0.5") "42.20 must declare mod version 1.0.5."
+Assert-True ($rootInfo -contains "modversion=1.0.5") "Root and release mod versions must match."
+
+$baselineFiles = Get-RelativeFileNames $baselineRoot
+$releaseFiles = Get-RelativeFileNames $releaseRoot
+Assert-True (-not (Compare-Object $baselineFiles $releaseFiles)) "42.20 file layout must match the latest baseline."
+
+foreach ($jsonFile in Get-ChildItem -LiteralPath $releaseRoot -Recurse -Filter "*.json") {
+ try {
+ $null = Get-Content -LiteralPath $jsonFile.FullName -Raw | ConvertFrom-Json
+ }
+ catch {
+ $failures.Add("Invalid JSON: $($jsonFile.FullName)")
+ }
+}
+
+$serverCommandsPath = Join-Path $releaseRoot "media/lua/server/TowingCommands.lua"
+$btTowPath = Join-Path $releaseRoot "media/lua/server/BTTow.lua"
+$clientSyncPath = Join-Path $releaseRoot "media/lua/client/TowBar/TowSyncClient.lua"
+$hookingPath = Join-Path $releaseRoot "media/lua/client/TowBar/TowingHooking.lua"
+$towbarTemplatePath = Join-Path $releaseRoot "media/scripts/vehicles/template_towbar.txt"
+$serverCommands = Get-Content -LiteralPath $serverCommandsPath -Raw
+$btTow = Get-Content -LiteralPath $btTowPath -Raw
+$clientSync = Get-Content -LiteralPath $clientSyncPath -Raw
+$hooking = Get-Content -LiteralPath $hookingPath -Raw
+$towbarTemplate = Get-Content -LiteralPath $towbarTemplatePath -Raw
+
+Assert-True ($serverCommands -match 'detachTrailerSpontaneous') "Server must handle Build 42.20 spontaneous towing detach."
+Assert-True ($serverCommands -match 'towBarTowedVehicleId') "Server must persist the expected towbar pair."
+Assert-True ($serverCommands -match '(?s)local function markExpectedTowBarPair.*?towingModData\["towBarTowingVehicleId"\] = nil.*?towedModData\["towBarTowedVehicleId"\] = nil') "Pair marking must clear stale opposite-role IDs."
+Assert-True ($serverCommands -match '(?s)local function clearExpectedTowBarPair.*?towingModData\["towBarTowedVehicleId"\] = nil.*?towingModData\["towBarTowingVehicleId"\] = nil.*?towedModData\["towBarTowedVehicleId"\] = nil.*?towedModData\["towBarTowingVehicleId"\] = nil') "Pair cleanup must clear both role IDs from both vehicles."
+Assert-True ($serverCommands -match 'local function isExpectedTowBarPair') "Server must validate reciprocal expected towbar IDs."
+Assert-True ($serverCommands -match '(?s)isExpectedTowBarPair.*?towBarTowedVehicleId.*?vehicleB:getId\(\).*?towBarTowingVehicleId.*?vehicleA:getId\(\)') "Expected towbar pair validation must be reciprocal."
+Assert-True ($serverCommands -match 'local function isLegacyTowBarPair') "Server must isolate legacy pair recovery from stale modern pair IDs."
+Assert-True ($serverCommands -match '(?s)isLegacyTowBarPair.*?hasTowBarState\(vehicleA\) and hasTowBarState\(vehicleB\).*?towed.*?== true.*?towingModData\["towBarTowedVehicleId"\].*?== nil.*?towingModData\["towBarTowingVehicleId"\].*?== nil.*?towedModData\["towBarTowedVehicleId"\].*?== nil.*?towedModData\["towBarTowingVehicleId"\].*?== nil') "Legacy recovery must require both role markers and no modern pair IDs on either vehicle."
+Assert-True ($serverCommands -match '(?s)local function resolveExpectedTowBarPair.*?towingVehicle and towedVehicle.*?isExpectedTowBarPair\(towingVehicle, towedVehicle\)') "Current-link spontaneous resolution must still require reciprocal towbar identity."
+Assert-True ($serverCommands -match '(?s)local function snapshotActiveTowbarLinksServer.*?isExpectedTowBarPair\(towingVehicle, towedVehicle\).*?isLegacyTowBarPair\(towingVehicle, towedVehicle\)') "Server snapshots must only preserve reciprocal or clean legacy towbar pairs."
+Assert-True ($serverCommands -match 'spontaneousDetachSync') "Server must broadcast spontaneous towbar cleanup."
+Assert-True ($serverCommands -match 'local vehicleBId = args\.vehicleB or args\.vehicle') "Manual detach sync must include the towed vehicle id."
+Assert-True ($serverCommands -notmatch 'function Commands\.giveTowBar') "Clients must not be able to invoke the towbar refund helper."
+Assert-True ($serverCommands -match 'isPlayerAuthorizedForPair') "Server refunds must validate the requesting player."
+Assert-True ($serverCommands -match 'rejected towbar attach without item') "Multiplayer attach must require server-side towbar ownership."
+Assert-True ($serverCommands -match '(?s)function Commands\.attachTowBar.*?if hasAnyTowLink\(vehicleA\) or hasAnyTowLink\(vehicleB\) then.*?return.*?if isExpectedTowBarPair\(vehicleA, vehicleB\).*?or isExpectedTowBarPair\(vehicleB, vehicleA\).*?or hasPendingAttach\(vehicleA, vehicleB\).*?or hasPendingAttach\(vehicleB, vehicleA\).*?then.*?return.*?local towBarItem = findTowBarItem') "Existing or reversed pending tow pairs must be rejected before consuming another towbar."
+Assert-True ($serverCommands -match 'queueSync\("attach", player, args, true\)') "Multiplayer attach must reserve the towbar for delayed confirmation."
+Assert-True ($serverCommands -match '(?s)function Commands\.attachTowBar.*?markExpectedTowBarPair\(vehicleA, vehicleB, args\.attachmentA, args\.attachmentB\).*?queueSync\("attach", player, args, true\).*?vehicleA:addPointConstraint') "Attach must record and queue the pair before constraint creation can break spontaneously."
+Assert-True ($serverCommands -match 'local function failAttachSync') "Failed multiplayer attach must have a cleanup/refund path."
+Assert-True ($serverCommands -match '(?s)local function processAttachSync.*?if isTowBarPairConfirmed\(vehicleA, vehicleB\) then return "broken" end') "A confirmed pair that breaks during attach confirmation must not be reattached."
+Assert-True ($serverCommands -match '(?s)local function processPendingSync.*?status == "broken".*?finalizeBrokenTowBarPair') "A break during attach confirmation must use the shared ground-drop finalizer."
+Assert-True ($serverCommands -match 'if item\.reservedTowBar then') "Failed multiplayer attach must refund the reserved towbar exactly once."
+Assert-True ($serverCommands -match 'Constraint creation may complete on a later server tick') "Multiplayer attach must preserve delayed constraint confirmation."
+Assert-True ($serverCommands -match 'local function finalizeBrokenTowBarPair') "Broken towbar cleanup must use one idempotent finalizer."
+Assert-True ($serverCommands -match 'local function reconcileBrokenTowBarPairsServer') "Server and single-player must audit confirmed towbar constraints for silent breaks."
+Assert-True ($serverCommands -match '(?s)local function reconcileBrokenTowBarPairsServer.*?towBarTowedVehicleId.*?getVehicleById.*?isExpectedTowBarPair.*?isLinked.*?markTowBarPairConfirmed.*?isTowBarPairConfirmed.*?finalizeBrokenTowBarPair') "The physical-link audit must confirm reciprocal pairs and finalize a missing constraint."
+Assert-True ($serverCommands -match '(?s)local function processPendingSync.*?reconcileBrokenTowBarPairsServer\(\)') "The physical-link audit must run from the server/SP tick handler."
+Assert-True ($serverCommands -match '(?s)local function finalizeBrokenTowBarPair.*?dropTowBarOnGround.*?cancelPendingAttach.*?breakTowBarConstraint.*?clearExpectedTowBarPair.*?broadcastSpontaneousDetach') "Broken-pair finalization must drop, disconnect, clear state, and synchronize cleanup."
+Assert-True ($serverCommands -match '(?s)local function finalizeBrokenTowBarPair.*?if isServer\(\) then.*?elseif not isClient\(\).*?TowBarMod\.Hook\.cleanupDetachedTowBar') "Single-player breaks must directly restore the vehicle without relying on a server command."
+Assert-True ($clientSync -match 'spontaneousDetachSync') "Client sync must receive spontaneous towbar cleanup."
+Assert-True ($clientSync -match 'local function hasConflictingTowLink') "Client detach sync must detect links to unrelated vehicles."
+Assert-True ($clientSync -match 'local function breakTowBarPair') "Client detach sync must break only the requested pair."
+Assert-True ($clientSync -notmatch 'local function safeBreak') "Client detach sync must not use an unrestricted per-vehicle constraint break."
+Assert-True ($clientSync -match '(?s)local function applyDetachSync.*?hasConflictingTowLink\(vehicleA, vehicleB\).*?return.*?breakTowBarPair\(vehicleA, vehicleB\).*?cleanupDetachedTowBar') "Client detach sync must reject conflicting links before cleanup."
+Assert-True ($hooking -match 'function TowBarMod\.Hook\.cleanupDetachedTowBar') "Client must expose idempotent towbar cleanup."
+Assert-True ($hooking -match '(?s)if modData\.towBarOriginalParkingBrakeOn ~= nil then.*?setParkingBrakeOn.*?end.*?if modData\.towBarOriginalParkingBrake ~= nil then.*?setParkingBrake.*?end.*?if modData\.towBarOriginalHandbrake ~= nil then.*?setHandbrake.*?end') "Free-rolling state may only change brake controls whose original values were captured."
+Assert-True ($hooking -notmatch 'tryVehicleCall\(vehicle, "setBrake"') "Free-rolling state must not change an unrestorable brake control."
+Assert-True ($hooking -notmatch 'tryVehicleCall\(vehicle, "setBraking"') "Free-rolling state must not change an unrestorable braking control."
+
+Assert-True ($hooking -match '(?s)local function getTowbarModelSlot\(script\).*?if not isVanillaScale\(script\) then.*?return 0\s+end') "KI5 client rendering must select towbar slot 0."
+Assert-True ($btTow -match '(?s)local function getTowbarModelSlot\(script\).*?if not isVanillaScale\(script\) then.*?return 0\s+end') "KI5 part initialization must select towbar slot 0."
+Assert-True ($hooking -match '(?s)local function setTowBarModelVisible.*?local part = normalPart.*?part:setModelVisible\("towbar" \.\. index, true\)') "Automatic rendering must use the normal towbar part."
+Assert-True ($btTow -match '(?s)function BTtow\.Init\.towbar.*?local shouldShowOnThisPart = part:getId\(\) == "towbar"') "Automatic part initialization must only show the normal towbar part."
+Assert-True ($hooking -notmatch 'local part = isVanilla and normalPart or largePart') "KI5 automatic rendering must not select the large towbar part."
+$normalTowbarPart = $towbarTemplate.Substring($towbarTemplate.IndexOf('part towbar'), $towbarTemplate.IndexOf('part towbarLarge') - $towbarTemplate.IndexOf('part towbar'))
+Assert-True ($normalTowbarPart -match '(?s)model towbar0\s*\{\s*file = towbarModel,\s*offset = 0 -0\.3 1\.0,') "Normal towbar0 must retain the radial-menu Z=1.0 geometry."
+Assert-True ($hooking -match 'function TowBarMod\.Hook\.setVehicleScriptWithTowBarHidden') "Vehicle script swaps must use the anti-flicker helper."
+Assert-True ($hooking -match '(?s)setVehicleScriptWithTowBarHidden.*?setTowBarModelVisible\(vehicle, false\).*?towBarModelSwapInProgress.*?vehicle:setScriptName\(scriptName\).*?setTowBarModelVisible\(vehicle, false\)') "The anti-flicker helper must hide towbar models for the full script swap."
+Assert-True ($btTow -match '(?s)function BTtow\.Init\.towbar.*?setModelVisible\("towbar" \.\. j, false\).*?towBarModelSwapInProgress.*?return') "Part initialization must not show a towbar during a script swap."
+Assert-True ($hooking -notmatch 'towedVehicle:setScriptName\(') "All hook script swaps must use the anti-flicker helper."
+Assert-True ($clientSync -notmatch 'vehicleB:setScriptName\(') "Client reconciliation must not bypass the anti-flicker helper."
+Assert-True ($clientSync -match 'TowBarMod\.Hook\.setVehicleScriptWithTowBarHidden\(vehicleB, "notTowingA_Trailer"\)') "Client reconciliation must hide towbar models while changing scripts."
+
+$spontaneousStart = $serverCommands.IndexOf('if module == "vehicle" and command == "detachTrailerSpontaneous" then')
+$spontaneousEnd = $serverCommands.IndexOf('if module == "towbar" and Commands[command] then', $spontaneousStart)
+Assert-True ($spontaneousStart -ge 0 -and $spontaneousEnd -gt $spontaneousStart) "Could not isolate the spontaneous detach handler."
+if ($spontaneousStart -ge 0 -and $spontaneousEnd -gt $spontaneousStart) {
+ $spontaneousBranch = $serverCommands.Substring($spontaneousStart, $spontaneousEnd - $spontaneousStart)
+ Assert-True ([regex]::Matches($spontaneousBranch, 'finalizeBrokenTowBarPair\(towingVehicle, towedVehicle').Count -eq 1) "The multiplayer spontaneous event must use the shared broken-pair finalizer."
+ Assert-True ($spontaneousBranch -notmatch 'dropTowBarOnGround\(|breakTowBarConstraint\(|clearExpectedTowBarPair\(') "The spontaneous event must not bypass exactly-once finalization."
+ Assert-True ($spontaneousBranch -notmatch 'giveTowBar\(|inventory:AddItem|sendAddItemToContainer') "A spontaneously broken towbar must not be refunded to inventory."
+}
+Assert-True ($serverCommands -match '(?s)local function dropTowBarOnGround\(towingVehicle, towedVehicle\).*?getSquare\(\).*?AddWorldInventoryItem\(TowBarItemType, 0\.5, 0\.5, 0\)') "A broken towbar must be placed on the ground beside the tow pair."
+Assert-True ([regex]::Matches($serverCommands, 'dropTowBarOnGround\(').Count -eq 2) "Only the shared finalizer may call the one towbar ground-drop helper."
+
+$cleanupStart = $hooking.IndexOf('function TowBarMod.Hook.cleanupDetachedTowBar')
+$cleanupEnd = $hooking.IndexOf('function TowBarMod.Hook.performDetachTowBar', $cleanupStart)
+Assert-True ($cleanupStart -ge 0 -and $cleanupEnd -gt $cleanupStart) "Could not isolate client towbar cleanup."
+if ($cleanupStart -ge 0 -and $cleanupEnd -gt $cleanupStart) {
+ $cleanupBranch = $hooking.Substring($cleanupStart, $cleanupEnd - $cleanupStart)
+ Assert-True ($cleanupBranch -match 'updateAttachmentsOnDefaultValues') "Break cleanup must restore attachment offsets."
+ Assert-True ($cleanupBranch -match 'setVehicleScriptWithTowBarHidden.*?towBarOriginalScriptName') "Break cleanup must restore the original vehicle script."
+ Assert-True ($cleanupBranch -match 'restoreFreeRollingTowState') "Break cleanup must restore mass, brakes, and parking-brake state."
+ Assert-True ($cleanupBranch -match 'setTowBarModelVisible\(towedVehicle, false\)') "Break cleanup must hide the towbar model."
+ Assert-True ($cleanupBranch -match '(?s)towingModData\["towBarTowedVehicleId"\] = nil.*?towingModData\["towBarTowingVehicleId"\] = nil.*?towedModData\["towBarTowedVehicleId"\] = nil.*?towedModData\["towBarTowingVehicleId"\] = nil') "Client cleanup must remove stale IDs for either tow role."
+}
+
+$manualDetachStart = $serverCommands.IndexOf('function Commands.detachTowBar')
+$manualDetachEnd = $serverCommands.IndexOf('function Commands.consumeTowBar', $manualDetachStart)
+Assert-True ($manualDetachStart -ge 0 -and $manualDetachEnd -gt $manualDetachStart) "Could not isolate the manual detach handler."
+if ($manualDetachStart -ge 0 -and $manualDetachEnd -gt $manualDetachStart) {
+ $manualDetachBranch = $serverCommands.Substring($manualDetachStart, $manualDetachEnd - $manualDetachStart)
+ Assert-True ($manualDetachBranch -match 'if not isLinked\(towingVehicle, towedVehicle\) then') "Manual detach must reject mismatched or stale vehicle pairs."
+ Assert-True ($manualDetachBranch -match '(?s)not isExpectedTowBarPair\(towingVehicle, towedVehicle\).*?not isLegacyTowBarPair\(towingVehicle, towedVehicle\)') "Manual detach must reject non-towbar or stale linked pairs."
+ Assert-True ($manualDetachBranch -match 'breakTowBarConstraint\(towingVehicle, towedVehicle\)') "Manual detach must only break the validated tow pair."
+ Assert-True ($manualDetachBranch -match 'giveTowBar\(player, true\)') "Manual detach must continue returning the towbar to inventory."
+ Assert-True ($manualDetachBranch -notmatch 'dropTowBarOnGround') "Manual detach must not drop the towbar on the ground."
+}
+
+$vanillaVehicleCommandsPath = Join-Path $gameRoot "media/lua/server/Vehicles/VehicleCommands.lua"
+if (Test-Path -LiteralPath $vanillaVehicleCommandsPath) {
+ $vanillaVehicleCommands = Get-Content -LiteralPath $vanillaVehicleCommandsPath -Raw
+ Assert-True ($vanillaVehicleCommands -match 'function Commands\.attachTrailer') "Installed game lacks the expected attach command."
+ Assert-True ($vanillaVehicleCommands -match 'function Commands\.detachTrailerSpontaneous') "Installed game lacks the Build 42.20 spontaneous detach command."
+ Assert-True ($vanillaVehicleCommands -match 'addPointConstraint') "Installed game lacks the expected towing constraint API."
+}
+
+$luaFiles = Get-ChildItem -LiteralPath (Join-Path $releaseRoot "media/lua") -Recurse -Filter "*.lua" |
+ ForEach-Object FullName
+& npx --yes luaparse --quiet @luaFiles
+Assert-True ($LASTEXITCODE -eq 0) "One or more 42.20 Lua files failed to parse."
+
+if ($failures.Count -gt 0) {
+ $failures | ForEach-Object { Write-Error $_ }
+ exit 1
+}
+
+Write-Output "PASS: Build 42.20 metadata, layout, JSON, Lua, and towing compatibility checks."