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)