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."